MediaProvider.java revision f7cc647081f0421c0247de275fa0754b29938c07
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.providers.media; 18 19import android.app.SearchManager; 20import android.content.BroadcastReceiver; 21import android.content.ComponentName; 22import android.content.ContentProvider; 23import android.content.ContentProviderOperation; 24import android.content.ContentProviderResult; 25import android.content.ContentResolver; 26import android.content.ContentUris; 27import android.content.ContentValues; 28import android.content.Context; 29import android.content.Intent; 30import android.content.IntentFilter; 31import android.content.OperationApplicationException; 32import android.content.ServiceConnection; 33import android.content.SharedPreferences; 34import android.content.UriMatcher; 35import android.database.Cursor; 36import android.database.DatabaseUtils; 37import android.database.MatrixCursor; 38import android.database.SQLException; 39import android.database.sqlite.SQLiteDatabase; 40import android.database.sqlite.SQLiteOpenHelper; 41import android.database.sqlite.SQLiteQueryBuilder; 42import android.graphics.Bitmap; 43import android.graphics.BitmapFactory; 44import android.media.MediaFile; 45import android.media.MediaScanner; 46import android.media.MiniThumbFile; 47import android.mtp.MtpConstants; 48import android.net.Uri; 49import android.os.Binder; 50import android.os.Environment; 51import android.os.FileUtils; 52import android.os.Handler; 53import android.os.HandlerThread; 54import android.os.Message; 55import android.os.ParcelFileDescriptor; 56import android.os.Process; 57import android.os.RemoteException; 58import android.os.SystemProperties; 59import android.preference.PreferenceManager; 60import android.provider.BaseColumns; 61import android.provider.MediaStore; 62import android.provider.MediaStore.Audio; 63import android.provider.MediaStore.Files; 64import android.provider.MediaStore.Images; 65import android.provider.MediaStore.MediaColumns; 66import android.provider.MediaStore.Video; 67import android.provider.MediaStore.Files.FileColumns; 68import android.provider.MediaStore.Images.ImageColumns; 69import android.text.TextUtils; 70import android.util.Log; 71 72import java.io.File; 73import java.io.FileInputStream; 74import java.io.FileNotFoundException; 75import java.io.IOException; 76import java.io.OutputStream; 77import java.text.Collator; 78import java.util.ArrayList; 79import java.util.HashMap; 80import java.util.HashSet; 81import java.util.Iterator; 82import java.util.List; 83import java.util.PriorityQueue; 84import java.util.Stack; 85 86/** 87 * Media content provider. See {@link android.provider.MediaStore} for details. 88 * Separate databases are kept for each external storage card we see (using the 89 * card's ID as an index). The content visible at content://media/external/... 90 * changes with the card. 91 */ 92public class MediaProvider extends ContentProvider { 93 private static final Uri MEDIA_URI = Uri.parse("content://media"); 94 private static final Uri ALBUMART_URI = Uri.parse("content://media/external/audio/albumart"); 95 private static final int ALBUM_THUMB = 1; 96 private static final int IMAGE_THUMB = 2; 97 98 private static final HashMap<String, String> sArtistAlbumsMap = new HashMap<String, String>(); 99 private static final HashMap<String, String> sFolderArtMap = new HashMap<String, String>(); 100 101 // A HashSet of paths that are pending creation of album art thumbnails. 102 private HashSet mPendingThumbs = new HashSet(); 103 104 // A Stack of outstanding thumbnail requests. 105 private Stack mThumbRequestStack = new Stack(); 106 107 // The lock of mMediaThumbQueue protects both mMediaThumbQueue and mCurrentThumbRequest. 108 private MediaThumbRequest mCurrentThumbRequest = null; 109 private PriorityQueue<MediaThumbRequest> mMediaThumbQueue = 110 new PriorityQueue<MediaThumbRequest>(MediaThumbRequest.PRIORITY_NORMAL, 111 MediaThumbRequest.getComparator()); 112 113 private static String mExternalStoragePath; 114 private boolean mCaseInsensitivePaths; 115 116 // For compatibility with the approximately 0 apps that used mediaprovider search in 117 // releases 1.0, 1.1 or 1.5 118 private String[] mSearchColsLegacy = new String[] { 119 android.provider.BaseColumns._ID, 120 MediaStore.Audio.Media.MIME_TYPE, 121 "(CASE WHEN grouporder=1 THEN " + R.drawable.ic_search_category_music_artist + 122 " ELSE CASE WHEN grouporder=2 THEN " + R.drawable.ic_search_category_music_album + 123 " ELSE " + R.drawable.ic_search_category_music_song + " END END" + 124 ") AS " + SearchManager.SUGGEST_COLUMN_ICON_1, 125 "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2, 126 "text1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 127 "text1 AS " + SearchManager.SUGGEST_COLUMN_QUERY, 128 "CASE when grouporder=1 THEN data1 ELSE artist END AS data1", 129 "CASE when grouporder=1 THEN data2 ELSE " + 130 "CASE WHEN grouporder=2 THEN NULL ELSE album END END AS data2", 131 "match as ar", 132 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 133 "grouporder", 134 "NULL AS itemorder" // We should be sorting by the artist/album/title keys, but that 135 // column is not available here, and the list is already sorted. 136 }; 137 private String[] mSearchColsFancy = new String[] { 138 android.provider.BaseColumns._ID, 139 MediaStore.Audio.Media.MIME_TYPE, 140 MediaStore.Audio.Artists.ARTIST, 141 MediaStore.Audio.Albums.ALBUM, 142 MediaStore.Audio.Media.TITLE, 143 "data1", 144 "data2", 145 }; 146 // If this array gets changed, please update the constant below to point to the correct item. 147 private String[] mSearchColsBasic = new String[] { 148 android.provider.BaseColumns._ID, 149 MediaStore.Audio.Media.MIME_TYPE, 150 "(CASE WHEN grouporder=1 THEN " + R.drawable.ic_search_category_music_artist + 151 " ELSE CASE WHEN grouporder=2 THEN " + R.drawable.ic_search_category_music_album + 152 " ELSE " + R.drawable.ic_search_category_music_song + " END END" + 153 ") AS " + SearchManager.SUGGEST_COLUMN_ICON_1, 154 "text1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 155 "text1 AS " + SearchManager.SUGGEST_COLUMN_QUERY, 156 "(CASE WHEN grouporder=1 THEN '%1'" + // %1 gets replaced with localized string. 157 " ELSE CASE WHEN grouporder=3 THEN artist || ' - ' || album" + 158 " ELSE CASE WHEN text2!='" + MediaStore.UNKNOWN_STRING + "' THEN text2" + 159 " ELSE NULL END END END) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2, 160 SearchManager.SUGGEST_COLUMN_INTENT_DATA 161 }; 162 // Position of the TEXT_2 item in the above array. 163 private final int SEARCH_COLUMN_BASIC_TEXT2 = 5; 164 165 private static final String[] mMediaTableColumns = new String[] { 166 FileColumns._ID, 167 FileColumns.MEDIA_TYPE, 168 }; 169 170 private Uri mAlbumArtBaseUri = Uri.parse("content://media/external/audio/albumart"); 171 172 private BroadcastReceiver mUnmountReceiver = new BroadcastReceiver() { 173 @Override 174 public void onReceive(Context context, Intent intent) { 175 if (intent.getAction().equals(Intent.ACTION_MEDIA_EJECT)) { 176 // Remove the external volume and then notify all cursors backed by 177 // data on that volume 178 detachVolume(Uri.parse("content://media/external")); 179 sFolderArtMap.clear(); 180 MiniThumbFile.reset(); 181 } 182 } 183 }; 184 185 // set to disable sending events when the operation originates from MTP 186 private boolean mDisableMtpObjectCallbacks; 187 188 private final SQLiteDatabase.CustomFunction mObjectRemovedCallback = 189 new SQLiteDatabase.CustomFunction() { 190 public void callback(String[] args) { 191 // do nothing if the operation originated from MTP 192 if (mDisableMtpObjectCallbacks) return; 193 194 Log.d(TAG, "object removed " + args[0]); 195 IMtpService mtpService = mMtpService; 196 if (mtpService != null) { 197 try { 198 sendObjectRemoved(Integer.parseInt(args[0])); 199 } catch (NumberFormatException e) { 200 Log.e(TAG, "NumberFormatException in mObjectRemovedCallback", e); 201 } 202 } 203 } 204 }; 205 206 /** 207 * Wrapper class for a specific database (associated with one particular 208 * external card, or with internal storage). Can open the actual database 209 * on demand, create and upgrade the schema, etc. 210 */ 211 private final class DatabaseHelper extends SQLiteOpenHelper { 212 final Context mContext; 213 final String mName; 214 final boolean mInternal; // True if this is the internal database 215 boolean mUpgradeAttempted; // Used for upgrade error handling 216 217 // In memory caches of artist and album data. 218 HashMap<String, Long> mArtistCache = new HashMap<String, Long>(); 219 HashMap<String, Long> mAlbumCache = new HashMap<String, Long>(); 220 221 public DatabaseHelper(Context context, String name, boolean internal) { 222 super(context, name, null, DATABASE_VERSION); 223 mContext = context; 224 mName = name; 225 mInternal = internal; 226 } 227 228 /** 229 * Creates database the first time we try to open it. 230 */ 231 @Override 232 public void onCreate(final SQLiteDatabase db) { 233 updateDatabase(db, mInternal, 0, DATABASE_VERSION); 234 } 235 236 /** 237 * Updates the database format when a new content provider is used 238 * with an older database format. 239 */ 240 @Override 241 public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) { 242 mUpgradeAttempted = true; 243 updateDatabase(db, mInternal, oldV, newV); 244 } 245 246 @Override 247 public synchronized SQLiteDatabase getWritableDatabase() { 248 SQLiteDatabase result = null; 249 mUpgradeAttempted = false; 250 try { 251 result = super.getWritableDatabase(); 252 } catch (Exception e) { 253 if (!mUpgradeAttempted) { 254 Log.e(TAG, "failed to open database " + mName, e); 255 return null; 256 } 257 } 258 259 // If we failed to open the database during an upgrade, delete the file and try again. 260 // This will result in the creation of a fresh database, which will be repopulated 261 // when the media scanner runs. 262 if (result == null && mUpgradeAttempted) { 263 mContext.getDatabasePath(mName).delete(); 264 result = super.getWritableDatabase(); 265 } 266 return result; 267 } 268 269 /** 270 * Touch this particular database and garbage collect old databases. 271 * An LRU cache system is used to clean up databases for old external 272 * storage volumes. 273 */ 274 @Override 275 public void onOpen(SQLiteDatabase db) { 276 277 // Turn off WAL optimization as it appears to cause difficult to repoduce 278 // disk i/o errors. 279 db.disableWriteAheadLogging(); 280 281 if (mInternal) return; // The internal database is kept separately. 282 283 db.addCustomFunction("_OBJECT_REMOVED", 1, mObjectRemovedCallback); 284 285 // touch the database file to show it is most recently used 286 File file = new File(db.getPath()); 287 long now = System.currentTimeMillis(); 288 file.setLastModified(now); 289 290 // delete least recently used databases if we are over the limit 291 String[] databases = mContext.databaseList(); 292 int count = databases.length; 293 int limit = MAX_EXTERNAL_DATABASES; 294 295 // delete external databases that have not been used in the past two months 296 long twoMonthsAgo = now - OBSOLETE_DATABASE_DB; 297 for (int i = 0; i < databases.length; i++) { 298 File other = mContext.getDatabasePath(databases[i]); 299 if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) { 300 databases[i] = null; 301 count--; 302 if (file.equals(other)) { 303 // reduce limit to account for the existence of the database we 304 // are about to open, which we removed from the list. 305 limit--; 306 } 307 } else { 308 long time = other.lastModified(); 309 if (time < twoMonthsAgo) { 310 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[i]); 311 mContext.deleteDatabase(databases[i]); 312 databases[i] = null; 313 count--; 314 } 315 } 316 } 317 318 // delete least recently used databases until 319 // we are no longer over the limit 320 while (count > limit) { 321 int lruIndex = -1; 322 long lruTime = 0; 323 324 for (int i = 0; i < databases.length; i++) { 325 if (databases[i] != null) { 326 long time = mContext.getDatabasePath(databases[i]).lastModified(); 327 if (lruTime == 0 || time < lruTime) { 328 lruIndex = i; 329 lruTime = time; 330 } 331 } 332 } 333 334 // delete least recently used database 335 if (lruIndex != -1) { 336 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]); 337 mContext.deleteDatabase(databases[lruIndex]); 338 databases[lruIndex] = null; 339 count--; 340 } 341 } 342 } 343 } 344 345 private IMtpService mMtpService; 346 347 private final ServiceConnection mMtpServiceConnection = new ServiceConnection() { 348 public void onServiceConnected(ComponentName className, android.os.IBinder service) { 349 Log.d(TAG, "mMtpService connected"); 350 mMtpService = IMtpService.Stub.asInterface(service); 351 } 352 353 public void onServiceDisconnected(ComponentName className) { 354 Log.d(TAG, "mMtpService disconnected"); 355 mMtpService = null; 356 } 357 }; 358 359 private static final String[] sDefaultFolderNames = { 360 Environment.DIRECTORY_MUSIC, 361 Environment.DIRECTORY_PODCASTS, 362 Environment.DIRECTORY_RINGTONES, 363 Environment.DIRECTORY_ALARMS, 364 Environment.DIRECTORY_NOTIFICATIONS, 365 Environment.DIRECTORY_PICTURES, 366 Environment.DIRECTORY_MOVIES, 367 Environment.DIRECTORY_DOWNLOADS, 368 Environment.DIRECTORY_DCIM, 369 }; 370 371 // creates default folders (Music, Downloads, etc) 372 private void createDefaultFolders(SQLiteDatabase db) { 373 // Use a SharedPreference to ensure we only do this once. 374 // We don't want to annoy the user by recreating the directories 375 // after she has deleted them. 376 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 377 if (prefs.getInt("created_default_folders", 0) == 0) { 378 for (String folderName : sDefaultFolderNames) { 379 File file = Environment.getExternalStoragePublicDirectory(folderName); 380 if (!file.exists()) { 381 file.mkdirs(); 382 insertDirectory(db, file.getAbsolutePath()); 383 } 384 } 385 386 SharedPreferences.Editor e = prefs.edit(); 387 e.clear(); 388 e.putInt("created_default_folders", 1); 389 e.commit(); 390 } 391 } 392 393 @Override 394 public boolean onCreate() { 395 final Context context = getContext(); 396 397 sArtistAlbumsMap.put(MediaStore.Audio.Albums._ID, "audio.album_id AS " + 398 MediaStore.Audio.Albums._ID); 399 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM, "album"); 400 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_KEY, "album_key"); 401 sArtistAlbumsMap.put(MediaStore.Audio.Albums.FIRST_YEAR, "MIN(year) AS " + 402 MediaStore.Audio.Albums.FIRST_YEAR); 403 sArtistAlbumsMap.put(MediaStore.Audio.Albums.LAST_YEAR, "MAX(year) AS " + 404 MediaStore.Audio.Albums.LAST_YEAR); 405 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST, "artist"); 406 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_ID, "artist"); 407 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_KEY, "artist_key"); 408 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS, "count(*) AS " + 409 MediaStore.Audio.Albums.NUMBER_OF_SONGS); 410 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_ART, "album_art._data AS " + 411 MediaStore.Audio.Albums.ALBUM_ART); 412 413 mSearchColsBasic[SEARCH_COLUMN_BASIC_TEXT2] = 414 mSearchColsBasic[SEARCH_COLUMN_BASIC_TEXT2].replaceAll( 415 "%1", context.getString(R.string.artist_label)); 416 mDatabases = new HashMap<String, DatabaseHelper>(); 417 attachVolume(INTERNAL_VOLUME); 418 419 IntentFilter iFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); 420 iFilter.addDataScheme("file"); 421 context.registerReceiver(mUnmountReceiver, iFilter); 422 423 mExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath(); 424 mCaseInsensitivePaths = !context.getResources().getBoolean( 425 com.android.internal.R.bool.config_caseSensitiveExternalStorage); 426 if (!Process.supportsProcesses()) { 427 // Simulator uses host file system, so it should be case sensitive. 428 mCaseInsensitivePaths = false; 429 } 430 431 // open external database if external storage is mounted 432 String state = Environment.getExternalStorageState(); 433 if (Environment.MEDIA_MOUNTED.equals(state) || 434 Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 435 attachVolume(EXTERNAL_VOLUME); 436 } 437 438 HandlerThread ht = new HandlerThread("thumbs thread", Process.THREAD_PRIORITY_BACKGROUND); 439 ht.start(); 440 mThumbHandler = new Handler(ht.getLooper()) { 441 @Override 442 public void handleMessage(Message msg) { 443 if (msg.what == IMAGE_THUMB) { 444 synchronized (mMediaThumbQueue) { 445 mCurrentThumbRequest = mMediaThumbQueue.poll(); 446 } 447 if (mCurrentThumbRequest == null) { 448 Log.w(TAG, "Have message but no request?"); 449 } else { 450 try { 451 File origFile = new File(mCurrentThumbRequest.mPath); 452 if (origFile.exists() && origFile.length() > 0) { 453 mCurrentThumbRequest.execute(); 454 } else { 455 // original file hasn't been stored yet 456 synchronized (mMediaThumbQueue) { 457 Log.w(TAG, "original file hasn't been stored yet: " + mCurrentThumbRequest.mPath); 458 } 459 } 460 } catch (IOException ex) { 461 Log.w(TAG, ex); 462 } catch (UnsupportedOperationException ex) { 463 // This could happen if we unplug the sd card during insert/update/delete 464 // See getDatabaseForUri. 465 Log.w(TAG, ex); 466 } catch (OutOfMemoryError err) { 467 /* 468 * Note: Catching Errors is in most cases considered 469 * bad practice. However, in this case it is 470 * motivated by the fact that corrupt or very large 471 * images may cause a huge allocation to be 472 * requested and denied. The bitmap handling API in 473 * Android offers no other way to guard against 474 * these problems than by catching OutOfMemoryError. 475 */ 476 Log.w(TAG, err); 477 } finally { 478 synchronized (mCurrentThumbRequest) { 479 mCurrentThumbRequest.mState = MediaThumbRequest.State.DONE; 480 mCurrentThumbRequest.notifyAll(); 481 } 482 } 483 } 484 } else if (msg.what == ALBUM_THUMB) { 485 ThumbData d; 486 synchronized (mThumbRequestStack) { 487 d = (ThumbData)mThumbRequestStack.pop(); 488 } 489 490 makeThumbInternal(d); 491 synchronized (mPendingThumbs) { 492 mPendingThumbs.remove(d.path); 493 } 494 } 495 } 496 }; 497 498 context.bindService(new Intent(context, MtpService.class), 499 mMtpServiceConnection, Context.BIND_AUTO_CREATE); 500 return true; 501 } 502 503 private static final String IMAGE_COLUMNS = 504 "_data,_size,_display_name,mime_type,title,date_added," + 505 "date_modified,description,picasa_id,isprivate,latitude,longitude," + 506 "datetaken,orientation,mini_thumb_magic,bucket_id,bucket_display_name"; 507 508 private static final String AUDIO_COLUMNSv99 = 509 "_data,_display_name,_size,mime_type,date_added," + 510 "date_modified,title,title_key,duration,artist_id,composer,album_id," + 511 "track,year,is_ringtone,is_music,is_alarm,is_notification,is_podcast," + 512 "bookmark"; 513 514 private static final String AUDIO_COLUMNSv100 = 515 "_data,_display_name,_size,mime_type,date_added," + 516 "date_modified,title,title_key,duration,artist_id,composer,album_id," + 517 "track,year,is_ringtone,is_music,is_alarm,is_notification,is_podcast," + 518 "bookmark,album_artist"; 519 520 private static final String VIDEO_COLUMNS = 521 "_data,_display_name,_size,mime_type,date_added,date_modified," + 522 "title,duration,artist,album,resolution,description,isprivate,tags," + 523 "category,language,mini_thumb_data,latitude,longitude,datetaken," + 524 "mini_thumb_magic,bucket_id,bucket_display_name, bookmark"; 525 526 private static final String PLAYLIST_COLUMNS = "_data,name,date_added,date_modified"; 527 528 /** 529 * This method takes care of updating all the tables in the database to the 530 * current version, creating them if necessary. 531 * This method can only update databases at schema 63 or higher, which was 532 * created August 1, 2008. Older database will be cleared and recreated. 533 * @param db Database 534 * @param internal True if this is the internal media database 535 */ 536 private static void updateDatabase(SQLiteDatabase db, boolean internal, 537 int fromVersion, int toVersion) { 538 539 // sanity checks 540 if (toVersion != DATABASE_VERSION) { 541 Log.e(TAG, "Illegal update request. Got " + toVersion + ", expected " + 542 DATABASE_VERSION); 543 throw new IllegalArgumentException(); 544 } else if (fromVersion > toVersion) { 545 Log.e(TAG, "Illegal update request: can't downgrade from " + fromVersion + 546 " to " + toVersion + ". Did you forget to wipe data?"); 547 throw new IllegalArgumentException(); 548 } 549 550 // Revisions 84-86 were a failed attempt at supporting the "album artist" id3 tag. 551 // We can't downgrade from those revisions, so start over. 552 // (the initial change to do this was wrong, so now we actually need to start over 553 // if the database version is 84-89) 554 // Post-gingerbread, revisions 91-94 were broken in a way that is not easy to repair. 555 // However version 91 was reused in a divergent development path for gingerbread, 556 // so we need to support upgrades from 91. 557 // Therefore we will only force a reset for versions 92 - 94. 558 if (fromVersion < 63 || (fromVersion >= 84 && fromVersion <= 89) || 559 (fromVersion >= 92 && fromVersion <= 94)) { 560 fromVersion = 63; 561 // Drop everything and start over. 562 Log.i(TAG, "Upgrading media database from version " + 563 fromVersion + " to " + toVersion + ", which will destroy all old data"); 564 db.execSQL("DROP TABLE IF EXISTS images"); 565 db.execSQL("DROP TRIGGER IF EXISTS images_cleanup"); 566 db.execSQL("DROP TABLE IF EXISTS thumbnails"); 567 db.execSQL("DROP TRIGGER IF EXISTS thumbnails_cleanup"); 568 db.execSQL("DROP TABLE IF EXISTS audio_meta"); 569 db.execSQL("DROP TABLE IF EXISTS artists"); 570 db.execSQL("DROP TABLE IF EXISTS albums"); 571 db.execSQL("DROP TABLE IF EXISTS album_art"); 572 db.execSQL("DROP VIEW IF EXISTS artist_info"); 573 db.execSQL("DROP VIEW IF EXISTS album_info"); 574 db.execSQL("DROP VIEW IF EXISTS artists_albums_map"); 575 db.execSQL("DROP TRIGGER IF EXISTS audio_meta_cleanup"); 576 db.execSQL("DROP TABLE IF EXISTS audio_genres"); 577 db.execSQL("DROP TABLE IF EXISTS audio_genres_map"); 578 db.execSQL("DROP TRIGGER IF EXISTS audio_genres_cleanup"); 579 db.execSQL("DROP TABLE IF EXISTS audio_playlists"); 580 db.execSQL("DROP TABLE IF EXISTS audio_playlists_map"); 581 db.execSQL("DROP TRIGGER IF EXISTS audio_playlists_cleanup"); 582 db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup1"); 583 db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup2"); 584 db.execSQL("DROP TABLE IF EXISTS video"); 585 db.execSQL("DROP TRIGGER IF EXISTS video_cleanup"); 586 db.execSQL("DROP TABLE IF EXISTS objects"); 587 db.execSQL("DROP TRIGGER IF EXISTS images_objects_cleanup"); 588 db.execSQL("DROP TRIGGER IF EXISTS audio_objects_cleanup"); 589 db.execSQL("DROP TRIGGER IF EXISTS video_objects_cleanup"); 590 db.execSQL("DROP TRIGGER IF EXISTS playlists_objects_cleanup"); 591 592 db.execSQL("CREATE TABLE IF NOT EXISTS images (" + 593 "_id INTEGER PRIMARY KEY," + 594 "_data TEXT," + 595 "_size INTEGER," + 596 "_display_name TEXT," + 597 "mime_type TEXT," + 598 "title TEXT," + 599 "date_added INTEGER," + 600 "date_modified INTEGER," + 601 "description TEXT," + 602 "picasa_id TEXT," + 603 "isprivate INTEGER," + 604 "latitude DOUBLE," + 605 "longitude DOUBLE," + 606 "datetaken INTEGER," + 607 "orientation INTEGER," + 608 "mini_thumb_magic INTEGER," + 609 "bucket_id TEXT," + 610 "bucket_display_name TEXT" + 611 ");"); 612 613 db.execSQL("CREATE INDEX IF NOT EXISTS mini_thumb_magic_index on images(mini_thumb_magic);"); 614 615 db.execSQL("CREATE TRIGGER IF NOT EXISTS images_cleanup DELETE ON images " + 616 "BEGIN " + 617 "DELETE FROM thumbnails WHERE image_id = old._id;" + 618 "SELECT _DELETE_FILE(old._data);" + 619 "END"); 620 621 // create image thumbnail table 622 db.execSQL("CREATE TABLE IF NOT EXISTS thumbnails (" + 623 "_id INTEGER PRIMARY KEY," + 624 "_data TEXT," + 625 "image_id INTEGER," + 626 "kind INTEGER," + 627 "width INTEGER," + 628 "height INTEGER" + 629 ");"); 630 631 db.execSQL("CREATE INDEX IF NOT EXISTS image_id_index on thumbnails(image_id);"); 632 633 db.execSQL("CREATE TRIGGER IF NOT EXISTS thumbnails_cleanup DELETE ON thumbnails " + 634 "BEGIN " + 635 "SELECT _DELETE_FILE(old._data);" + 636 "END"); 637 638 // Contains meta data about audio files 639 db.execSQL("CREATE TABLE IF NOT EXISTS audio_meta (" + 640 "_id INTEGER PRIMARY KEY," + 641 "_data TEXT UNIQUE NOT NULL," + 642 "_display_name TEXT," + 643 "_size INTEGER," + 644 "mime_type TEXT," + 645 "date_added INTEGER," + 646 "date_modified INTEGER," + 647 "title TEXT NOT NULL," + 648 "title_key TEXT NOT NULL," + 649 "duration INTEGER," + 650 "artist_id INTEGER," + 651 "composer TEXT," + 652 "album_id INTEGER," + 653 "track INTEGER," + // track is an integer to allow proper sorting 654 "year INTEGER CHECK(year!=0)," + 655 "is_ringtone INTEGER," + 656 "is_music INTEGER," + 657 "is_alarm INTEGER," + 658 "is_notification INTEGER" + 659 ");"); 660 661 // Contains a sort/group "key" and the preferred display name for artists 662 db.execSQL("CREATE TABLE IF NOT EXISTS artists (" + 663 "artist_id INTEGER PRIMARY KEY," + 664 "artist_key TEXT NOT NULL UNIQUE," + 665 "artist TEXT NOT NULL" + 666 ");"); 667 668 // Contains a sort/group "key" and the preferred display name for albums 669 db.execSQL("CREATE TABLE IF NOT EXISTS albums (" + 670 "album_id INTEGER PRIMARY KEY," + 671 "album_key TEXT NOT NULL UNIQUE," + 672 "album TEXT NOT NULL" + 673 ");"); 674 675 db.execSQL("CREATE TABLE IF NOT EXISTS album_art (" + 676 "album_id INTEGER PRIMARY KEY," + 677 "_data TEXT" + 678 ");"); 679 680 recreateAudioView(db); 681 682 683 // Provides some extra info about artists, like the number of tracks 684 // and albums for this artist 685 db.execSQL("CREATE VIEW IF NOT EXISTS artist_info AS " + 686 "SELECT artist_id AS _id, artist, artist_key, " + 687 "COUNT(DISTINCT album) AS number_of_albums, " + 688 "COUNT(*) AS number_of_tracks FROM audio WHERE is_music=1 "+ 689 "GROUP BY artist_key;"); 690 691 // Provides extra info albums, such as the number of tracks 692 db.execSQL("CREATE VIEW IF NOT EXISTS album_info AS " + 693 "SELECT audio.album_id AS _id, album, album_key, " + 694 "MIN(year) AS minyear, " + 695 "MAX(year) AS maxyear, artist, artist_id, artist_key, " + 696 "count(*) AS " + MediaStore.Audio.Albums.NUMBER_OF_SONGS + 697 ",album_art._data AS album_art" + 698 " FROM audio LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id" + 699 " WHERE is_music=1 GROUP BY audio.album_id;"); 700 701 // For a given artist_id, provides the album_id for albums on 702 // which the artist appears. 703 db.execSQL("CREATE VIEW IF NOT EXISTS artists_albums_map AS " + 704 "SELECT DISTINCT artist_id, album_id FROM audio_meta;"); 705 706 /* 707 * Only external media volumes can handle genres, playlists, etc. 708 */ 709 if (!internal) { 710 // Cleans up when an audio file is deleted 711 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_meta_cleanup DELETE ON audio_meta " + 712 "BEGIN " + 713 "DELETE FROM audio_genres_map WHERE audio_id = old._id;" + 714 "DELETE FROM audio_playlists_map WHERE audio_id = old._id;" + 715 "END"); 716 717 // Contains audio genre definitions 718 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres (" + 719 "_id INTEGER PRIMARY KEY," + 720 "name TEXT NOT NULL" + 721 ");"); 722 723 // Contiains mappings between audio genres and audio files 724 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres_map (" + 725 "_id INTEGER PRIMARY KEY," + 726 "audio_id INTEGER NOT NULL," + 727 "genre_id INTEGER NOT NULL" + 728 ");"); 729 730 // Cleans up when an audio genre is delete 731 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_genres_cleanup DELETE ON audio_genres " + 732 "BEGIN " + 733 "DELETE FROM audio_genres_map WHERE genre_id = old._id;" + 734 "END"); 735 736 // Contains audio playlist definitions 737 db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists (" + 738 "_id INTEGER PRIMARY KEY," + 739 "_data TEXT," + // _data is path for file based playlists, or null 740 "name TEXT NOT NULL," + 741 "date_added INTEGER," + 742 "date_modified INTEGER" + 743 ");"); 744 745 // Contains mappings between audio playlists and audio files 746 db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists_map (" + 747 "_id INTEGER PRIMARY KEY," + 748 "audio_id INTEGER NOT NULL," + 749 "playlist_id INTEGER NOT NULL," + 750 "play_order INTEGER NOT NULL" + 751 ");"); 752 753 // Cleans up when an audio playlist is deleted 754 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_playlists_cleanup DELETE ON audio_playlists " + 755 "BEGIN " + 756 "DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" + 757 "SELECT _DELETE_FILE(old._data);" + 758 "END"); 759 760 // Cleans up album_art table entry when an album is deleted 761 db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup1 DELETE ON albums " + 762 "BEGIN " + 763 "DELETE FROM album_art WHERE album_id = old.album_id;" + 764 "END"); 765 766 // Cleans up album_art when an album is deleted 767 db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup2 DELETE ON album_art " + 768 "BEGIN " + 769 "SELECT _DELETE_FILE(old._data);" + 770 "END"); 771 } 772 773 // Contains meta data about video files 774 db.execSQL("CREATE TABLE IF NOT EXISTS video (" + 775 "_id INTEGER PRIMARY KEY," + 776 "_data TEXT NOT NULL," + 777 "_display_name TEXT," + 778 "_size INTEGER," + 779 "mime_type TEXT," + 780 "date_added INTEGER," + 781 "date_modified INTEGER," + 782 "title TEXT," + 783 "duration INTEGER," + 784 "artist TEXT," + 785 "album TEXT," + 786 "resolution TEXT," + 787 "description TEXT," + 788 "isprivate INTEGER," + // for YouTube videos 789 "tags TEXT," + // for YouTube videos 790 "category TEXT," + // for YouTube videos 791 "language TEXT," + // for YouTube videos 792 "mini_thumb_data TEXT," + 793 "latitude DOUBLE," + 794 "longitude DOUBLE," + 795 "datetaken INTEGER," + 796 "mini_thumb_magic INTEGER" + 797 ");"); 798 799 db.execSQL("CREATE TRIGGER IF NOT EXISTS video_cleanup DELETE ON video " + 800 "BEGIN " + 801 "SELECT _DELETE_FILE(old._data);" + 802 "END"); 803 } 804 805 // At this point the database is at least at schema version 63 (it was 806 // either created at version 63 by the code above, or was already at 807 // version 63 or later) 808 809 if (fromVersion < 64) { 810 // create the index that updates the database to schema version 64 811 db.execSQL("CREATE INDEX IF NOT EXISTS sort_index on images(datetaken ASC, _id ASC);"); 812 } 813 814 /* 815 * Android 1.0 shipped with database version 64 816 */ 817 818 if (fromVersion < 65) { 819 // create the index that updates the database to schema version 65 820 db.execSQL("CREATE INDEX IF NOT EXISTS titlekey_index on audio_meta(title_key);"); 821 } 822 823 // In version 66, originally we updateBucketNames(db, "images"), 824 // but we need to do it in version 89 and therefore save the update here. 825 826 if (fromVersion < 67) { 827 // create the indices that update the database to schema version 67 828 db.execSQL("CREATE INDEX IF NOT EXISTS albumkey_index on albums(album_key);"); 829 db.execSQL("CREATE INDEX IF NOT EXISTS artistkey_index on artists(artist_key);"); 830 } 831 832 if (fromVersion < 68) { 833 // Create bucket_id and bucket_display_name columns for the video table. 834 db.execSQL("ALTER TABLE video ADD COLUMN bucket_id TEXT;"); 835 db.execSQL("ALTER TABLE video ADD COLUMN bucket_display_name TEXT"); 836 837 // In version 68, originally we updateBucketNames(db, "video"), 838 // but we need to do it in version 89 and therefore save the update here. 839 } 840 841 if (fromVersion < 69) { 842 updateDisplayName(db, "images"); 843 } 844 845 if (fromVersion < 70) { 846 // Create bookmark column for the video table. 847 db.execSQL("ALTER TABLE video ADD COLUMN bookmark INTEGER;"); 848 } 849 850 if (fromVersion < 71) { 851 // There is no change to the database schema, however a code change 852 // fixed parsing of metadata for certain files bought from the 853 // iTunes music store, so we want to rescan files that might need it. 854 // We do this by clearing the modification date in the database for 855 // those files, so that the media scanner will see them as updated 856 // and rescan them. 857 db.execSQL("UPDATE audio_meta SET date_modified=0 WHERE _id IN (" + 858 "SELECT _id FROM audio where mime_type='audio/mp4' AND " + 859 "artist='" + MediaStore.UNKNOWN_STRING + "' AND " + 860 "album='" + MediaStore.UNKNOWN_STRING + "'" + 861 ");"); 862 } 863 864 if (fromVersion < 72) { 865 // Create is_podcast and bookmark columns for the audio table. 866 db.execSQL("ALTER TABLE audio_meta ADD COLUMN is_podcast INTEGER;"); 867 db.execSQL("UPDATE audio_meta SET is_podcast=1 WHERE _data LIKE '%/podcasts/%';"); 868 db.execSQL("UPDATE audio_meta SET is_music=0 WHERE is_podcast=1" + 869 " AND _data NOT LIKE '%/music/%';"); 870 db.execSQL("ALTER TABLE audio_meta ADD COLUMN bookmark INTEGER;"); 871 872 // New columns added to tables aren't visible in views on those tables 873 // without opening and closing the database (or using the 'vacuum' command, 874 // which we can't do here because all this code runs inside a transaction). 875 // To work around this, we drop and recreate the affected view and trigger. 876 recreateAudioView(db); 877 } 878 879 /* 880 * Android 1.5 shipped with database version 72 881 */ 882 883 if (fromVersion < 73) { 884 // There is no change to the database schema, but we now do case insensitive 885 // matching of folder names when determining whether something is music, a 886 // ringtone, podcast, etc, so we might need to reclassify some files. 887 db.execSQL("UPDATE audio_meta SET is_music=1 WHERE is_music=0 AND " + 888 "_data LIKE '%/music/%';"); 889 db.execSQL("UPDATE audio_meta SET is_ringtone=1 WHERE is_ringtone=0 AND " + 890 "_data LIKE '%/ringtones/%';"); 891 db.execSQL("UPDATE audio_meta SET is_notification=1 WHERE is_notification=0 AND " + 892 "_data LIKE '%/notifications/%';"); 893 db.execSQL("UPDATE audio_meta SET is_alarm=1 WHERE is_alarm=0 AND " + 894 "_data LIKE '%/alarms/%';"); 895 db.execSQL("UPDATE audio_meta SET is_podcast=1 WHERE is_podcast=0 AND " + 896 "_data LIKE '%/podcasts/%';"); 897 } 898 899 if (fromVersion < 74) { 900 // This view is used instead of the audio view by the union below, to force 901 // sqlite to use the title_key index. This greatly reduces memory usage 902 // (no separate copy pass needed for sorting, which could cause errors on 903 // large datasets) and improves speed (by about 35% on a large dataset) 904 db.execSQL("CREATE VIEW IF NOT EXISTS searchhelpertitle AS SELECT * FROM audio " + 905 "ORDER BY title_key;"); 906 907 db.execSQL("CREATE VIEW IF NOT EXISTS search AS " + 908 "SELECT _id," + 909 "'artist' AS mime_type," + 910 "artist," + 911 "NULL AS album," + 912 "NULL AS title," + 913 "artist AS text1," + 914 "NULL AS text2," + 915 "number_of_albums AS data1," + 916 "number_of_tracks AS data2," + 917 "artist_key AS match," + 918 "'content://media/external/audio/artists/'||_id AS suggest_intent_data," + 919 "1 AS grouporder " + 920 "FROM artist_info WHERE (artist!='" + MediaStore.UNKNOWN_STRING + "') " + 921 "UNION ALL " + 922 "SELECT _id," + 923 "'album' AS mime_type," + 924 "artist," + 925 "album," + 926 "NULL AS title," + 927 "album AS text1," + 928 "artist AS text2," + 929 "NULL AS data1," + 930 "NULL AS data2," + 931 "artist_key||' '||album_key AS match," + 932 "'content://media/external/audio/albums/'||_id AS suggest_intent_data," + 933 "2 AS grouporder " + 934 "FROM album_info WHERE (album!='" + MediaStore.UNKNOWN_STRING + "') " + 935 "UNION ALL " + 936 "SELECT searchhelpertitle._id AS _id," + 937 "mime_type," + 938 "artist," + 939 "album," + 940 "title," + 941 "title AS text1," + 942 "artist AS text2," + 943 "NULL AS data1," + 944 "NULL AS data2," + 945 "artist_key||' '||album_key||' '||title_key AS match," + 946 "'content://media/external/audio/media/'||searchhelpertitle._id AS " + 947 "suggest_intent_data," + 948 "3 AS grouporder " + 949 "FROM searchhelpertitle WHERE (title != '') " 950 ); 951 } 952 953 if (fromVersion < 75) { 954 // Force a rescan of the audio entries so we can apply the new logic to 955 // distinguish same-named albums. 956 db.execSQL("UPDATE audio_meta SET date_modified=0;"); 957 db.execSQL("DELETE FROM albums"); 958 } 959 960 if (fromVersion < 76) { 961 // We now ignore double quotes when building the key, so we have to remove all of them 962 // from existing keys. 963 db.execSQL("UPDATE audio_meta SET title_key=" + 964 "REPLACE(title_key,x'081D08C29F081D',x'081D') " + 965 "WHERE title_key LIKE '%'||x'081D08C29F081D'||'%';"); 966 db.execSQL("UPDATE albums SET album_key=" + 967 "REPLACE(album_key,x'081D08C29F081D',x'081D') " + 968 "WHERE album_key LIKE '%'||x'081D08C29F081D'||'%';"); 969 db.execSQL("UPDATE artists SET artist_key=" + 970 "REPLACE(artist_key,x'081D08C29F081D',x'081D') " + 971 "WHERE artist_key LIKE '%'||x'081D08C29F081D'||'%';"); 972 } 973 974 /* 975 * Android 1.6 shipped with database version 76 976 */ 977 978 if (fromVersion < 77) { 979 // create video thumbnail table 980 db.execSQL("CREATE TABLE IF NOT EXISTS videothumbnails (" + 981 "_id INTEGER PRIMARY KEY," + 982 "_data TEXT," + 983 "video_id INTEGER," + 984 "kind INTEGER," + 985 "width INTEGER," + 986 "height INTEGER" + 987 ");"); 988 989 db.execSQL("CREATE INDEX IF NOT EXISTS video_id_index on videothumbnails(video_id);"); 990 991 db.execSQL("CREATE TRIGGER IF NOT EXISTS videothumbnails_cleanup DELETE ON videothumbnails " + 992 "BEGIN " + 993 "SELECT _DELETE_FILE(old._data);" + 994 "END"); 995 } 996 997 /* 998 * Android 2.0 and 2.0.1 shipped with database version 77 999 */ 1000 1001 if (fromVersion < 78) { 1002 // Force a rescan of the video entries so we can update 1003 // latest changed DATE_TAKEN units (in milliseconds). 1004 db.execSQL("UPDATE video SET date_modified=0;"); 1005 } 1006 1007 /* 1008 * Android 2.1 shipped with database version 78 1009 */ 1010 1011 if (fromVersion < 79) { 1012 // move /sdcard/albumthumbs to 1013 // /sdcard/Android/data/com.android.providers.media/albumthumbs, 1014 // and update the database accordingly 1015 1016 String oldthumbspath = mExternalStoragePath + "/albumthumbs"; 1017 String newthumbspath = mExternalStoragePath + "/" + ALBUM_THUMB_FOLDER; 1018 File thumbsfolder = new File(oldthumbspath); 1019 if (thumbsfolder.exists()) { 1020 // move folder to its new location 1021 File newthumbsfolder = new File(newthumbspath); 1022 newthumbsfolder.getParentFile().mkdirs(); 1023 if(thumbsfolder.renameTo(newthumbsfolder)) { 1024 // update the database 1025 db.execSQL("UPDATE album_art SET _data=REPLACE(_data, '" + 1026 oldthumbspath + "','" + newthumbspath + "');"); 1027 } 1028 } 1029 } 1030 1031 if (fromVersion < 80) { 1032 // Force rescan of image entries to update DATE_TAKEN as UTC timestamp. 1033 db.execSQL("UPDATE images SET date_modified=0;"); 1034 } 1035 1036 if (fromVersion < 81 && !internal) { 1037 // Delete entries starting with /mnt/sdcard. This is for the benefit 1038 // of users running builds between 2.0.1 and 2.1 final only, since 1039 // users updating from 2.0 or earlier will not have such entries. 1040 1041 // First we need to update the _data fields in the affected tables, since 1042 // otherwise deleting the entries will also delete the underlying files 1043 // (via a trigger), and we want to keep them. 1044 db.execSQL("UPDATE audio_playlists SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1045 db.execSQL("UPDATE images SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1046 db.execSQL("UPDATE video SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1047 db.execSQL("UPDATE videothumbnails SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1048 db.execSQL("UPDATE thumbnails SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1049 db.execSQL("UPDATE album_art SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1050 db.execSQL("UPDATE audio_meta SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1051 // Once the paths have been renamed, we can safely delete the entries 1052 db.execSQL("DELETE FROM audio_playlists WHERE _data IS '////';"); 1053 db.execSQL("DELETE FROM images WHERE _data IS '////';"); 1054 db.execSQL("DELETE FROM video WHERE _data IS '////';"); 1055 db.execSQL("DELETE FROM videothumbnails WHERE _data IS '////';"); 1056 db.execSQL("DELETE FROM thumbnails WHERE _data IS '////';"); 1057 db.execSQL("DELETE FROM audio_meta WHERE _data IS '////';"); 1058 db.execSQL("DELETE FROM album_art WHERE _data IS '////';"); 1059 1060 // rename existing entries starting with /sdcard to /mnt/sdcard 1061 db.execSQL("UPDATE audio_meta" + 1062 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1063 db.execSQL("UPDATE audio_playlists" + 1064 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1065 db.execSQL("UPDATE images" + 1066 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1067 db.execSQL("UPDATE video" + 1068 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1069 db.execSQL("UPDATE videothumbnails" + 1070 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1071 db.execSQL("UPDATE thumbnails" + 1072 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1073 db.execSQL("UPDATE album_art" + 1074 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1075 1076 // Delete albums and artists, then clear the modification time on songs, which 1077 // will cause the media scanner to rescan everything, rebuilding the artist and 1078 // album tables along the way, while preserving playlists. 1079 // We need this rescan because ICU also changed, and now generates different 1080 // collation keys 1081 db.execSQL("DELETE from albums"); 1082 db.execSQL("DELETE from artists"); 1083 db.execSQL("UPDATE audio_meta SET date_modified=0;"); 1084 } 1085 1086 if (fromVersion < 82) { 1087 // recreate this view with the correct "group by" specifier 1088 db.execSQL("DROP VIEW IF EXISTS artist_info"); 1089 db.execSQL("CREATE VIEW IF NOT EXISTS artist_info AS " + 1090 "SELECT artist_id AS _id, artist, artist_key, " + 1091 "COUNT(DISTINCT album_key) AS number_of_albums, " + 1092 "COUNT(*) AS number_of_tracks FROM audio WHERE is_music=1 "+ 1093 "GROUP BY artist_key;"); 1094 } 1095 1096 /* we skipped over version 83, and reverted versions 84, 85 and 86 */ 1097 1098 if (fromVersion < 87) { 1099 // The fastscroll thumb needs an index on the strings being displayed, 1100 // otherwise the queries it does to determine the correct position 1101 // becomes really inefficient 1102 db.execSQL("CREATE INDEX IF NOT EXISTS title_idx on audio_meta(title);"); 1103 db.execSQL("CREATE INDEX IF NOT EXISTS artist_idx on artists(artist);"); 1104 db.execSQL("CREATE INDEX IF NOT EXISTS album_idx on albums(album);"); 1105 } 1106 1107 if (fromVersion < 88) { 1108 // Clean up a few more things from versions 84/85/86, and recreate 1109 // the few things worth keeping from those changes. 1110 db.execSQL("DROP TRIGGER IF EXISTS albums_update1;"); 1111 db.execSQL("DROP TRIGGER IF EXISTS albums_update2;"); 1112 db.execSQL("DROP TRIGGER IF EXISTS albums_update3;"); 1113 db.execSQL("DROP TRIGGER IF EXISTS albums_update4;"); 1114 db.execSQL("DROP TRIGGER IF EXISTS artist_update1;"); 1115 db.execSQL("DROP TRIGGER IF EXISTS artist_update2;"); 1116 db.execSQL("DROP TRIGGER IF EXISTS artist_update3;"); 1117 db.execSQL("DROP TRIGGER IF EXISTS artist_update4;"); 1118 db.execSQL("DROP VIEW IF EXISTS album_artists;"); 1119 db.execSQL("CREATE INDEX IF NOT EXISTS album_id_idx on audio_meta(album_id);"); 1120 db.execSQL("CREATE INDEX IF NOT EXISTS artist_id_idx on audio_meta(artist_id);"); 1121 // For a given artist_id, provides the album_id for albums on 1122 // which the artist appears. 1123 db.execSQL("CREATE VIEW IF NOT EXISTS artists_albums_map AS " + 1124 "SELECT DISTINCT artist_id, album_id FROM audio_meta;"); 1125 } 1126 1127 // In version 89, originally we updateBucketNames(db, "images") and 1128 // updateBucketNames(db, "video"), but in version 101 we now updateBucketNames 1129 // for all files and therefore can save the update here. 1130 1131 if (fromVersion < 91) { 1132 // Never query by mini_thumb_magic_index 1133 db.execSQL("DROP INDEX IF EXISTS mini_thumb_magic_index"); 1134 1135 // sort the items by taken date in each bucket 1136 db.execSQL("CREATE INDEX IF NOT EXISTS image_bucket_index ON images(bucket_id, datetaken)"); 1137 db.execSQL("CREATE INDEX IF NOT EXISTS video_bucket_index ON video(bucket_id, datetaken)"); 1138 } 1139 1140 // versions 92 - 98 were work in progress on MTP obsoleted by version 99 1141 if (fromVersion < 92) { 1142 // Delete albums and artists, then clear the modification time on songs, which 1143 // will cause the media scanner to rescan everything, rebuilding the artist and 1144 // album tables along the way, while preserving playlists. 1145 // We need this rescan because ICU also changed, and now generates different 1146 // collation keys 1147 db.execSQL("DELETE from albums"); 1148 db.execSQL("DELETE from artists"); 1149 db.execSQL("UPDATE audio_meta SET date_modified=0;"); 1150 } 1151 1152 if (fromVersion < 99) { 1153 // Remove various stages of work in progress for MTP support 1154 db.execSQL("DROP TABLE IF EXISTS objects"); 1155 db.execSQL("DROP TABLE IF EXISTS files"); 1156 db.execSQL("DROP TRIGGER IF EXISTS images_objects_cleanup;"); 1157 db.execSQL("DROP TRIGGER IF EXISTS audio_objects_cleanup;"); 1158 db.execSQL("DROP TRIGGER IF EXISTS video_objects_cleanup;"); 1159 db.execSQL("DROP TRIGGER IF EXISTS playlists_objects_cleanup;"); 1160 db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_images;"); 1161 db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_audio;"); 1162 db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_video;"); 1163 db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_playlists;"); 1164 db.execSQL("DROP TRIGGER IF EXISTS media_cleanup;"); 1165 1166 // Create a new table to manage all files in our storage. 1167 // This contains a union of all the columns from the old 1168 // images, audio_meta, videos and audio_playlist tables. 1169 db.execSQL("CREATE TABLE files (" + 1170 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 1171 "_data TEXT," + // this can be null for playlists 1172 "_size INTEGER," + 1173 "format INTEGER," + 1174 "parent INTEGER," + 1175 "date_added INTEGER," + 1176 "date_modified INTEGER," + 1177 "mime_type TEXT," + 1178 "title TEXT," + 1179 "description TEXT," + 1180 "_display_name TEXT," + 1181 1182 // for images 1183 "picasa_id TEXT," + 1184 "orientation INTEGER," + 1185 1186 // for images and video 1187 "latitude DOUBLE," + 1188 "longitude DOUBLE," + 1189 "datetaken INTEGER," + 1190 "mini_thumb_magic INTEGER," + 1191 "bucket_id TEXT," + 1192 "bucket_display_name TEXT," + 1193 "isprivate INTEGER," + 1194 1195 // for audio 1196 "title_key TEXT," + 1197 "artist_id INTEGER," + 1198 "album_id INTEGER," + 1199 "composer TEXT," + 1200 "track INTEGER," + 1201 "year INTEGER CHECK(year!=0)," + 1202 "is_ringtone INTEGER," + 1203 "is_music INTEGER," + 1204 "is_alarm INTEGER," + 1205 "is_notification INTEGER," + 1206 "is_podcast INTEGER," + 1207 1208 // for audio and video 1209 "duration INTEGER," + 1210 "bookmark INTEGER," + 1211 1212 // for video 1213 "artist TEXT," + 1214 "album TEXT," + 1215 "resolution TEXT," + 1216 "tags TEXT," + 1217 "category TEXT," + 1218 "language TEXT," + 1219 "mini_thumb_data TEXT," + 1220 1221 // for playlists 1222 "name TEXT," + 1223 1224 // media_type is used by the views to emulate the old 1225 // images, audio_meta, videos and audio_playlist tables. 1226 "media_type INTEGER," + 1227 1228 // Value of _id from the old media table. 1229 // Used only for updating other tables during database upgrade. 1230 "old_id INTEGER" + 1231 ");"); 1232 db.execSQL("CREATE INDEX path_index ON files(_data);"); 1233 db.execSQL("CREATE INDEX media_type_index ON files(media_type);"); 1234 1235 // Copy all data from our obsolete tables to the new files table 1236 db.execSQL("INSERT INTO files (" + IMAGE_COLUMNS + ",old_id,media_type) SELECT " 1237 + IMAGE_COLUMNS + ",_id," + FileColumns.MEDIA_TYPE_IMAGE + " FROM images;"); 1238 db.execSQL("INSERT INTO files (" + AUDIO_COLUMNSv99 + ",old_id,media_type) SELECT " 1239 + AUDIO_COLUMNSv99 + ",_id," + FileColumns.MEDIA_TYPE_AUDIO + " FROM audio_meta;"); 1240 db.execSQL("INSERT INTO files (" + VIDEO_COLUMNS + ",old_id,media_type) SELECT " 1241 + VIDEO_COLUMNS + ",_id," + FileColumns.MEDIA_TYPE_VIDEO + " FROM video;"); 1242 if (!internal) { 1243 db.execSQL("INSERT INTO files (" + PLAYLIST_COLUMNS + ",old_id,media_type) SELECT " 1244 + PLAYLIST_COLUMNS + ",_id," + FileColumns.MEDIA_TYPE_PLAYLIST 1245 + " FROM audio_playlists;"); 1246 } 1247 1248 // Delete the old tables 1249 db.execSQL("DROP TABLE IF EXISTS images"); 1250 db.execSQL("DROP TABLE IF EXISTS audio_meta"); 1251 db.execSQL("DROP TABLE IF EXISTS video"); 1252 db.execSQL("DROP TABLE IF EXISTS audio_playlists"); 1253 1254 // Create views to replace our old tables 1255 db.execSQL("CREATE VIEW images AS SELECT _id," + IMAGE_COLUMNS + 1256 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1257 + FileColumns.MEDIA_TYPE_IMAGE + ";"); 1258// audio_meta will be created below for schema 100 1259// db.execSQL("CREATE VIEW audio_meta AS SELECT _id," + AUDIO_COLUMNSv99 + 1260// " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1261// + FileColumns.MEDIA_TYPE_AUDIO + ";"); 1262 db.execSQL("CREATE VIEW video AS SELECT _id," + VIDEO_COLUMNS + 1263 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1264 + FileColumns.MEDIA_TYPE_VIDEO + ";"); 1265 if (!internal) { 1266 db.execSQL("CREATE VIEW audio_playlists AS SELECT _id," + PLAYLIST_COLUMNS + 1267 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1268 + FileColumns.MEDIA_TYPE_PLAYLIST + ";"); 1269 } 1270 1271 // update the image_id column in the thumbnails table. 1272 db.execSQL("UPDATE thumbnails SET image_id = (SELECT _id FROM files " 1273 + "WHERE files.old_id = thumbnails.image_id AND files.media_type = " 1274 + FileColumns.MEDIA_TYPE_IMAGE + ");"); 1275 1276 if (!internal) { 1277 // update audio_id in the audio_genres_map and audio_playlists_map tables. 1278 db.execSQL("UPDATE audio_genres_map SET audio_id = (SELECT _id FROM files " 1279 + "WHERE files.old_id = audio_genres_map.audio_id AND files.media_type = " 1280 + FileColumns.MEDIA_TYPE_AUDIO + ");"); 1281 db.execSQL("UPDATE audio_playlists_map SET audio_id = (SELECT _id FROM files " 1282 + "WHERE files.old_id = audio_playlists_map.audio_id " 1283 + "AND files.media_type = " + FileColumns.MEDIA_TYPE_AUDIO + ");"); 1284 } 1285 1286 // update video_id in the videothumbnails table. 1287 db.execSQL("UPDATE videothumbnails SET video_id = (SELECT _id FROM files " 1288 + "WHERE files.old_id = videothumbnails.video_id AND files.media_type = " 1289 + FileColumns.MEDIA_TYPE_VIDEO + ");"); 1290 1291 // update indices to work on the files table 1292 db.execSQL("DROP INDEX IF EXISTS title_idx"); 1293 db.execSQL("DROP INDEX IF EXISTS album_id_idx"); 1294 db.execSQL("DROP INDEX IF EXISTS image_bucket_index"); 1295 db.execSQL("DROP INDEX IF EXISTS video_bucket_index"); 1296 db.execSQL("DROP INDEX IF EXISTS sort_index"); 1297 db.execSQL("DROP INDEX IF EXISTS titlekey_index"); 1298 db.execSQL("DROP INDEX IF EXISTS artist_id_idx"); 1299 db.execSQL("CREATE INDEX title_idx ON files(title);"); 1300 db.execSQL("CREATE INDEX album_id_idx ON files(album_id);"); 1301 db.execSQL("CREATE INDEX bucket_index ON files(bucket_id, datetaken);"); 1302 db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC);"); 1303 db.execSQL("CREATE INDEX titlekey_index ON files(title_key);"); 1304 db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id);"); 1305 1306 // Recreate triggers for our obsolete tables on the new files table 1307 db.execSQL("DROP TRIGGER IF EXISTS images_cleanup"); 1308 db.execSQL("DROP TRIGGER IF EXISTS audio_meta_cleanup"); 1309 db.execSQL("DROP TRIGGER IF EXISTS video_cleanup"); 1310 db.execSQL("DROP TRIGGER IF EXISTS audio_playlists_cleanup"); 1311 db.execSQL("DROP TRIGGER IF EXISTS audio_delete"); 1312 1313 db.execSQL("CREATE TRIGGER IF NOT EXISTS images_cleanup DELETE ON files " + 1314 "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_IMAGE + " " + 1315 "BEGIN " + 1316 "DELETE FROM thumbnails WHERE image_id = old._id;" + 1317 "SELECT _DELETE_FILE(old._data);" + 1318 "END"); 1319 1320 db.execSQL("CREATE TRIGGER IF NOT EXISTS video_cleanup DELETE ON files " + 1321 "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_VIDEO + " " + 1322 "BEGIN " + 1323 "SELECT _DELETE_FILE(old._data);" + 1324 "END"); 1325 1326 if (!internal) { 1327 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_meta_cleanup DELETE ON files " + 1328 "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_AUDIO + " " + 1329 "BEGIN " + 1330 "DELETE FROM audio_genres_map WHERE audio_id = old._id;" + 1331 "DELETE FROM audio_playlists_map WHERE audio_id = old._id;" + 1332 "END"); 1333 1334 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_playlists_cleanup DELETE ON files " + 1335 "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_PLAYLIST + " " + 1336 "BEGIN " + 1337 "DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" + 1338 "SELECT _DELETE_FILE(old._data);" + 1339 "END"); 1340 1341 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_delete INSTEAD OF DELETE ON audio " + 1342 "BEGIN " + 1343 "DELETE from files where _id=old._id;" + 1344 "DELETE from audio_playlists_map where audio_id=old._id;" + 1345 "DELETE from audio_genres_map where audio_id=old._id;" + 1346 "END"); 1347 } 1348 } 1349 1350 if (fromVersion < 100) { 1351 db.execSQL("ALTER TABLE files ADD COLUMN album_artist TEXT;"); 1352 db.execSQL("DROP VIEW IF EXISTS audio_meta;"); 1353 db.execSQL("CREATE VIEW audio_meta AS SELECT _id," + AUDIO_COLUMNSv100 + 1354 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1355 + FileColumns.MEDIA_TYPE_AUDIO + ";"); 1356 db.execSQL("UPDATE files SET date_modified=0 WHERE " + FileColumns.MEDIA_TYPE + "=" 1357 + FileColumns.MEDIA_TYPE_AUDIO + ";"); 1358 } 1359 1360 if (fromVersion < 300) { 1361 // we now compute bucket and display names for all files to avoid problems with files 1362 // that the media scanner might not recognize as images or videos 1363 updateBucketNames(db, "files"); 1364 } 1365 1366 if (fromVersion < 301) { 1367 db.execSQL("DROP INDEX IF EXISTS bucket_index"); 1368 db.execSQL("CREATE INDEX bucket_index on files(bucket_id, media_type, datetaken, _id)"); 1369 db.execSQL("CREATE INDEX bucket_name on files(bucket_id, media_type, bucket_display_name)"); 1370 } 1371 1372 if (fromVersion < 302) { 1373 db.execSQL("CREATE INDEX parent_index ON files(parent);"); 1374 db.execSQL("CREATE INDEX format_index ON files(format);"); 1375 } 1376 1377 sanityCheck(db, fromVersion); 1378 } 1379 1380 /** 1381 * Perform a simple sanity check on the database. Currently this tests 1382 * whether all the _data entries in audio_meta are unique 1383 */ 1384 private static void sanityCheck(SQLiteDatabase db, int fromVersion) { 1385 Cursor c1 = db.query("audio_meta", new String[] {"count(*)"}, 1386 null, null, null, null, null); 1387 Cursor c2 = db.query("audio_meta", new String[] {"count(distinct _data)"}, 1388 null, null, null, null, null); 1389 c1.moveToFirst(); 1390 c2.moveToFirst(); 1391 int num1 = c1.getInt(0); 1392 int num2 = c2.getInt(0); 1393 c1.close(); 1394 c2.close(); 1395 if (num1 != num2) { 1396 Log.e(TAG, "audio_meta._data column is not unique while upgrading" + 1397 " from schema " +fromVersion + " : " + num1 +"/" + num2); 1398 // Delete all audio_meta rows so they will be rebuilt by the media scanner 1399 db.execSQL("DELETE FROM audio_meta;"); 1400 } 1401 } 1402 1403 private static void recreateAudioView(SQLiteDatabase db) { 1404 // Provides a unified audio/artist/album info view. 1405 // Note that views are read-only, so we define a trigger to allow deletes. 1406 db.execSQL("DROP VIEW IF EXISTS audio"); 1407 db.execSQL("DROP TRIGGER IF EXISTS audio_delete"); 1408 db.execSQL("CREATE VIEW IF NOT EXISTS audio as SELECT * FROM audio_meta " + 1409 "LEFT OUTER JOIN artists ON audio_meta.artist_id=artists.artist_id " + 1410 "LEFT OUTER JOIN albums ON audio_meta.album_id=albums.album_id;"); 1411 1412 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_delete INSTEAD OF DELETE ON audio " + 1413 "BEGIN " + 1414 "DELETE from audio_meta where _id=old._id;" + 1415 "DELETE from audio_playlists_map where audio_id=old._id;" + 1416 "DELETE from audio_genres_map where audio_id=old._id;" + 1417 "END"); 1418 } 1419 1420 /** 1421 * Iterate through the rows of a table in a database, ensuring that the bucket_id and 1422 * bucket_display_name columns are correct. 1423 * @param db 1424 * @param tableName 1425 */ 1426 private static void updateBucketNames(SQLiteDatabase db, String tableName) { 1427 // Rebuild the bucket_display_name column using the natural case rather than lower case. 1428 db.beginTransaction(); 1429 try { 1430 String[] columns = {BaseColumns._ID, MediaColumns.DATA}; 1431 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 1432 try { 1433 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 1434 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 1435 while (cursor.moveToNext()) { 1436 String data = cursor.getString(dataColumnIndex); 1437 ContentValues values = new ContentValues(); 1438 computeBucketValues(data, values); 1439 int rowId = cursor.getInt(idColumnIndex); 1440 db.update(tableName, values, "_id=" + rowId, null); 1441 } 1442 } finally { 1443 cursor.close(); 1444 } 1445 db.setTransactionSuccessful(); 1446 } finally { 1447 db.endTransaction(); 1448 } 1449 } 1450 1451 /** 1452 * Iterate through the rows of a table in a database, ensuring that the 1453 * display name column has a value. 1454 * @param db 1455 * @param tableName 1456 */ 1457 private static void updateDisplayName(SQLiteDatabase db, String tableName) { 1458 // Fill in default values for null displayName values 1459 db.beginTransaction(); 1460 try { 1461 String[] columns = {BaseColumns._ID, MediaColumns.DATA, MediaColumns.DISPLAY_NAME}; 1462 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 1463 try { 1464 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 1465 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 1466 final int displayNameIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME); 1467 ContentValues values = new ContentValues(); 1468 while (cursor.moveToNext()) { 1469 String displayName = cursor.getString(displayNameIndex); 1470 if (displayName == null) { 1471 String data = cursor.getString(dataColumnIndex); 1472 values.clear(); 1473 computeDisplayName(data, values); 1474 int rowId = cursor.getInt(idColumnIndex); 1475 db.update(tableName, values, "_id=" + rowId, null); 1476 } 1477 } 1478 } finally { 1479 cursor.close(); 1480 } 1481 db.setTransactionSuccessful(); 1482 } finally { 1483 db.endTransaction(); 1484 } 1485 } 1486 /** 1487 * @param data The input path 1488 * @param values the content values, where the bucked id name and bucket display name are updated. 1489 * 1490 */ 1491 1492 private static void computeBucketValues(String data, ContentValues values) { 1493 File parentFile = new File(data).getParentFile(); 1494 if (parentFile == null) { 1495 parentFile = new File("/"); 1496 } 1497 1498 // Lowercase the path for hashing. This avoids duplicate buckets if the 1499 // filepath case is changed externally. 1500 // Keep the original case for display. 1501 String path = parentFile.toString().toLowerCase(); 1502 String name = parentFile.getName(); 1503 1504 // Note: the BUCKET_ID and BUCKET_DISPLAY_NAME attributes are spelled the 1505 // same for both images and video. However, for backwards-compatibility reasons 1506 // there is no common base class. We use the ImageColumns version here 1507 values.put(ImageColumns.BUCKET_ID, path.hashCode()); 1508 values.put(ImageColumns.BUCKET_DISPLAY_NAME, name); 1509 } 1510 1511 /** 1512 * @param data The input path 1513 * @param values the content values, where the display name is updated. 1514 * 1515 */ 1516 private static void computeDisplayName(String data, ContentValues values) { 1517 String s = (data == null ? "" : data.toString()); 1518 int idx = s.lastIndexOf('/'); 1519 if (idx >= 0) { 1520 s = s.substring(idx + 1); 1521 } 1522 values.put("_display_name", s); 1523 } 1524 1525 /** 1526 * Copy taken time from date_modified if we lost the original value (e.g. after factory reset) 1527 * This works for both video and image tables. 1528 * 1529 * @param values the content values, where taken time is updated. 1530 */ 1531 private static void computeTakenTime(ContentValues values) { 1532 if (! values.containsKey(Images.Media.DATE_TAKEN)) { 1533 // This only happens when MediaScanner finds an image file that doesn't have any useful 1534 // reference to get this value. (e.g. GPSTimeStamp) 1535 Long lastModified = values.getAsLong(MediaColumns.DATE_MODIFIED); 1536 if (lastModified != null) { 1537 values.put(Images.Media.DATE_TAKEN, lastModified * 1000); 1538 } 1539 } 1540 } 1541 1542 /** 1543 * This method blocks until thumbnail is ready. 1544 * 1545 * @param thumbUri 1546 * @return 1547 */ 1548 private boolean waitForThumbnailReady(Uri origUri) { 1549 Cursor c = this.query(origUri, new String[] { ImageColumns._ID, ImageColumns.DATA, 1550 ImageColumns.MINI_THUMB_MAGIC}, null, null, null); 1551 if (c == null) return false; 1552 1553 boolean result = false; 1554 1555 if (c.moveToFirst()) { 1556 long id = c.getLong(0); 1557 String path = c.getString(1); 1558 long magic = c.getLong(2); 1559 1560 MediaThumbRequest req = requestMediaThumbnail(path, origUri, 1561 MediaThumbRequest.PRIORITY_HIGH, magic); 1562 if (req == null) { 1563 return false; 1564 } 1565 synchronized (req) { 1566 try { 1567 while (req.mState == MediaThumbRequest.State.WAIT) { 1568 req.wait(); 1569 } 1570 } catch (InterruptedException e) { 1571 Log.w(TAG, e); 1572 } 1573 if (req.mState == MediaThumbRequest.State.DONE) { 1574 result = true; 1575 } 1576 } 1577 } 1578 c.close(); 1579 1580 return result; 1581 } 1582 1583 private boolean matchThumbRequest(MediaThumbRequest req, int pid, long id, long gid, 1584 boolean isVideo) { 1585 boolean cancelAllOrigId = (id == -1); 1586 boolean cancelAllGroupId = (gid == -1); 1587 return (req.mCallingPid == pid) && 1588 (cancelAllGroupId || req.mGroupId == gid) && 1589 (cancelAllOrigId || req.mOrigId == id) && 1590 (req.mIsVideo == isVideo); 1591 } 1592 1593 private boolean queryThumbnail(SQLiteQueryBuilder qb, Uri uri, String table, 1594 String column, boolean hasThumbnailId) { 1595 qb.setTables(table); 1596 if (hasThumbnailId) { 1597 // For uri dispatched to this method, the 4th path segment is always 1598 // the thumbnail id. 1599 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 1600 // client already knows which thumbnail it wants, bypass it. 1601 return true; 1602 } 1603 String origId = uri.getQueryParameter("orig_id"); 1604 // We can't query ready_flag unless we know original id 1605 if (origId == null) { 1606 // this could be thumbnail query for other purpose, bypass it. 1607 return true; 1608 } 1609 1610 boolean needBlocking = "1".equals(uri.getQueryParameter("blocking")); 1611 boolean cancelRequest = "1".equals(uri.getQueryParameter("cancel")); 1612 Uri origUri = uri.buildUpon().encodedPath( 1613 uri.getPath().replaceFirst("thumbnails", "media")) 1614 .appendPath(origId).build(); 1615 1616 if (needBlocking && !waitForThumbnailReady(origUri)) { 1617 Log.w(TAG, "original media doesn't exist or it's canceled."); 1618 return false; 1619 } else if (cancelRequest) { 1620 String groupId = uri.getQueryParameter("group_id"); 1621 boolean isVideo = "video".equals(uri.getPathSegments().get(1)); 1622 int pid = Binder.getCallingPid(); 1623 long id = -1; 1624 long gid = -1; 1625 1626 try { 1627 id = Long.parseLong(origId); 1628 gid = Long.parseLong(groupId); 1629 } catch (NumberFormatException ex) { 1630 // invalid cancel request 1631 return false; 1632 } 1633 1634 synchronized (mMediaThumbQueue) { 1635 if (mCurrentThumbRequest != null && 1636 matchThumbRequest(mCurrentThumbRequest, pid, id, gid, isVideo)) { 1637 synchronized (mCurrentThumbRequest) { 1638 mCurrentThumbRequest.mState = MediaThumbRequest.State.CANCEL; 1639 mCurrentThumbRequest.notifyAll(); 1640 } 1641 } 1642 for (MediaThumbRequest mtq : mMediaThumbQueue) { 1643 if (matchThumbRequest(mtq, pid, id, gid, isVideo)) { 1644 synchronized (mtq) { 1645 mtq.mState = MediaThumbRequest.State.CANCEL; 1646 mtq.notifyAll(); 1647 } 1648 1649 mMediaThumbQueue.remove(mtq); 1650 } 1651 } 1652 } 1653 } 1654 1655 if (origId != null) { 1656 qb.appendWhere(column + " = " + origId); 1657 } 1658 return true; 1659 } 1660 @SuppressWarnings("fallthrough") 1661 @Override 1662 public Cursor query(Uri uri, String[] projectionIn, String selection, 1663 String[] selectionArgs, String sort) { 1664 int table = URI_MATCHER.match(uri); 1665 1666 // Log.v(TAG, "query: uri="+uri+", selection="+selection); 1667 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1668 if (table == MEDIA_SCANNER) { 1669 if (mMediaScannerVolume == null) { 1670 return null; 1671 } else { 1672 // create a cursor to return volume currently being scanned by the media scanner 1673 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME}); 1674 c.addRow(new String[] {mMediaScannerVolume}); 1675 return c; 1676 } 1677 } 1678 1679 // Used temporarily (until we have unique media IDs) to get an identifier 1680 // for the current sd card, so that the music app doesn't have to use the 1681 // non-public getFatVolumeId method 1682 if (table == FS_ID) { 1683 MatrixCursor c = new MatrixCursor(new String[] {"fsid"}); 1684 c.addRow(new Integer[] {mVolumeId}); 1685 return c; 1686 } 1687 1688 String groupBy = null; 1689 DatabaseHelper database = getDatabaseForUri(uri); 1690 if (database == null) { 1691 return null; 1692 } 1693 SQLiteDatabase db = database.getReadableDatabase(); 1694 if (db == null) return null; 1695 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1696 String limit = uri.getQueryParameter("limit"); 1697 String filter = uri.getQueryParameter("filter"); 1698 String [] keywords = null; 1699 if (filter != null) { 1700 filter = Uri.decode(filter).trim(); 1701 if (!TextUtils.isEmpty(filter)) { 1702 String [] searchWords = filter.split(" "); 1703 keywords = new String[searchWords.length]; 1704 Collator col = Collator.getInstance(); 1705 col.setStrength(Collator.PRIMARY); 1706 for (int i = 0; i < searchWords.length; i++) { 1707 String key = MediaStore.Audio.keyFor(searchWords[i]); 1708 key = key.replace("\\", "\\\\"); 1709 key = key.replace("%", "\\%"); 1710 key = key.replace("_", "\\_"); 1711 keywords[i] = key; 1712 } 1713 } 1714 } 1715 if (uri.getQueryParameter("distinct") != null) { 1716 qb.setDistinct(true); 1717 } 1718 1719 boolean hasThumbnailId = false; 1720 1721 switch (table) { 1722 case IMAGES_MEDIA: 1723 qb.setTables("images"); 1724 if (uri.getQueryParameter("distinct") != null) 1725 qb.setDistinct(true); 1726 1727 // set the project map so that data dir is prepended to _data. 1728 //qb.setProjectionMap(mImagesProjectionMap, true); 1729 break; 1730 1731 case IMAGES_MEDIA_ID: 1732 qb.setTables("images"); 1733 if (uri.getQueryParameter("distinct") != null) 1734 qb.setDistinct(true); 1735 1736 // set the project map so that data dir is prepended to _data. 1737 //qb.setProjectionMap(mImagesProjectionMap, true); 1738 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 1739 break; 1740 1741 case IMAGES_THUMBNAILS_ID: 1742 hasThumbnailId = true; 1743 case IMAGES_THUMBNAILS: 1744 if (!queryThumbnail(qb, uri, "thumbnails", "image_id", hasThumbnailId)) { 1745 return null; 1746 } 1747 break; 1748 1749 case AUDIO_MEDIA: 1750 if (projectionIn != null && projectionIn.length == 1 && selectionArgs == null 1751 && (selection == null || selection.equalsIgnoreCase("is_music=1") 1752 || selection.equalsIgnoreCase("is_podcast=1") ) 1753 && projectionIn[0].equalsIgnoreCase("count(*)") 1754 && keywords != null) { 1755 //Log.i("@@@@", "taking fast path for counting songs"); 1756 qb.setTables("audio_meta"); 1757 } else { 1758 qb.setTables("audio"); 1759 for (int i = 0; keywords != null && i < keywords.length; i++) { 1760 if (i > 0) { 1761 qb.appendWhere(" AND "); 1762 } 1763 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 1764 "||" + MediaStore.Audio.Media.ALBUM_KEY + 1765 "||" + MediaStore.Audio.Media.TITLE_KEY + " LIKE '%" + 1766 keywords[i] + "%' ESCAPE '\\'"); 1767 } 1768 } 1769 break; 1770 1771 case AUDIO_MEDIA_ID: 1772 qb.setTables("audio"); 1773 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1774 break; 1775 1776 case AUDIO_MEDIA_ID_GENRES: 1777 qb.setTables("audio_genres"); 1778 qb.appendWhere("_id IN (SELECT genre_id FROM " + 1779 "audio_genres_map WHERE audio_id = " + 1780 uri.getPathSegments().get(3) + ")"); 1781 break; 1782 1783 case AUDIO_MEDIA_ID_GENRES_ID: 1784 qb.setTables("audio_genres"); 1785 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1786 break; 1787 1788 case AUDIO_MEDIA_ID_PLAYLISTS: 1789 qb.setTables("audio_playlists"); 1790 qb.appendWhere("_id IN (SELECT playlist_id FROM " + 1791 "audio_playlists_map WHERE audio_id = " + 1792 uri.getPathSegments().get(3) + ")"); 1793 break; 1794 1795 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1796 qb.setTables("audio_playlists"); 1797 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1798 break; 1799 1800 case AUDIO_GENRES: 1801 qb.setTables("audio_genres"); 1802 break; 1803 1804 case AUDIO_GENRES_ID: 1805 qb.setTables("audio_genres"); 1806 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1807 break; 1808 1809 case AUDIO_GENRES_ID_MEMBERS: 1810 qb.setTables("audio"); 1811 qb.appendWhere("_id IN (SELECT audio_id FROM " + 1812 "audio_genres_map WHERE genre_id = " + 1813 uri.getPathSegments().get(3) + ")"); 1814 break; 1815 1816 case AUDIO_GENRES_ID_MEMBERS_ID: 1817 qb.setTables("audio"); 1818 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1819 break; 1820 1821 case AUDIO_PLAYLISTS: 1822 qb.setTables("audio_playlists"); 1823 break; 1824 1825 case AUDIO_PLAYLISTS_ID: 1826 qb.setTables("audio_playlists"); 1827 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1828 break; 1829 1830 case AUDIO_PLAYLISTS_ID_MEMBERS: 1831 // if simpleQuery is true, we can do a simpler query on just audio_playlists_map 1832 // we can do this if we have no keywords and our projection includes just columns 1833 // from audio_playlists_map 1834 boolean simpleQuery = (keywords == null && projectionIn != null 1835 && (selection == null || selection.equalsIgnoreCase("playlist_id=?"))); 1836 if (projectionIn != null) { 1837 for (int i = 0; i < projectionIn.length; i++) { 1838 String p = projectionIn[i]; 1839 if (simpleQuery && !(p.equals("audio_id") || 1840 p.equals("playlist_id") || p.equals("play_order"))) { 1841 simpleQuery = false; 1842 } 1843 if (p.equals("_id")) { 1844 projectionIn[i] = "audio_playlists_map._id AS _id"; 1845 } 1846 } 1847 } 1848 if (simpleQuery) { 1849 qb.setTables("audio_playlists_map"); 1850 qb.appendWhere("playlist_id = " + uri.getPathSegments().get(3)); 1851 } else { 1852 qb.setTables("audio_playlists_map, audio"); 1853 qb.appendWhere("audio._id = audio_id AND playlist_id = " 1854 + uri.getPathSegments().get(3)); 1855 for (int i = 0; keywords != null && i < keywords.length; i++) { 1856 qb.appendWhere(" AND "); 1857 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 1858 "||" + MediaStore.Audio.Media.ALBUM_KEY + 1859 "||" + MediaStore.Audio.Media.TITLE_KEY + 1860 " LIKE '%" + keywords[i] + "%' ESCAPE '\\'"); 1861 } 1862 } 1863 break; 1864 1865 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1866 qb.setTables("audio"); 1867 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1868 break; 1869 1870 case VIDEO_MEDIA: 1871 qb.setTables("video"); 1872 break; 1873 case VIDEO_MEDIA_ID: 1874 qb.setTables("video"); 1875 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1876 break; 1877 1878 case VIDEO_THUMBNAILS_ID: 1879 hasThumbnailId = true; 1880 case VIDEO_THUMBNAILS: 1881 if (!queryThumbnail(qb, uri, "videothumbnails", "video_id", hasThumbnailId)) { 1882 return null; 1883 } 1884 break; 1885 1886 case AUDIO_ARTISTS: 1887 if (projectionIn != null && projectionIn.length == 1 && selectionArgs == null 1888 && (selection == null || selection.length() == 0) 1889 && projectionIn[0].equalsIgnoreCase("count(*)") 1890 && keywords != null) { 1891 //Log.i("@@@@", "taking fast path for counting artists"); 1892 qb.setTables("audio_meta"); 1893 projectionIn[0] = "count(distinct artist_id)"; 1894 qb.appendWhere("is_music=1"); 1895 } else { 1896 qb.setTables("artist_info"); 1897 for (int i = 0; keywords != null && i < keywords.length; i++) { 1898 if (i > 0) { 1899 qb.appendWhere(" AND "); 1900 } 1901 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 1902 " LIKE '%" + keywords[i] + "%' ESCAPE '\\'"); 1903 } 1904 } 1905 break; 1906 1907 case AUDIO_ARTISTS_ID: 1908 qb.setTables("artist_info"); 1909 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1910 break; 1911 1912 case AUDIO_ARTISTS_ID_ALBUMS: 1913 String aid = uri.getPathSegments().get(3); 1914 qb.setTables("audio LEFT OUTER JOIN album_art ON" + 1915 " audio.album_id=album_art.album_id"); 1916 qb.appendWhere("is_music=1 AND audio.album_id IN (SELECT album_id FROM " + 1917 "artists_albums_map WHERE artist_id = " + 1918 aid + ")"); 1919 for (int i = 0; keywords != null && i < keywords.length; i++) { 1920 qb.appendWhere(" AND "); 1921 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 1922 "||" + MediaStore.Audio.Media.ALBUM_KEY + 1923 " LIKE '%" + keywords[i] + "%' ESCAPE '\\'"); 1924 } 1925 groupBy = "audio.album_id"; 1926 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 1927 "count(CASE WHEN artist_id==" + aid + " THEN 'foo' ELSE NULL END) AS " + 1928 MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 1929 qb.setProjectionMap(sArtistAlbumsMap); 1930 break; 1931 1932 case AUDIO_ALBUMS: 1933 if (projectionIn != null && projectionIn.length == 1 && selectionArgs == null 1934 && (selection == null || selection.length() == 0) 1935 && projectionIn[0].equalsIgnoreCase("count(*)") 1936 && keywords != null) { 1937 //Log.i("@@@@", "taking fast path for counting albums"); 1938 qb.setTables("audio_meta"); 1939 projectionIn[0] = "count(distinct album_id)"; 1940 qb.appendWhere("is_music=1"); 1941 } else { 1942 qb.setTables("album_info"); 1943 for (int i = 0; keywords != null && i < keywords.length; i++) { 1944 if (i > 0) { 1945 qb.appendWhere(" AND "); 1946 } 1947 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 1948 "||" + MediaStore.Audio.Media.ALBUM_KEY + 1949 " LIKE '%" + keywords[i] + "%' ESCAPE '\\'"); 1950 } 1951 } 1952 break; 1953 1954 case AUDIO_ALBUMS_ID: 1955 qb.setTables("album_info"); 1956 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1957 break; 1958 1959 case AUDIO_ALBUMART_ID: 1960 qb.setTables("album_art"); 1961 qb.appendWhere("album_id=" + uri.getPathSegments().get(3)); 1962 break; 1963 1964 case AUDIO_SEARCH_LEGACY: 1965 Log.w(TAG, "Legacy media search Uri used. Please update your code."); 1966 // fall through 1967 case AUDIO_SEARCH_FANCY: 1968 case AUDIO_SEARCH_BASIC: 1969 return doAudioSearch(db, qb, uri, projectionIn, selection, selectionArgs, sort, 1970 table, limit); 1971 1972 case FILES_ID: 1973 case MTP_OBJECTS_ID: 1974 qb.appendWhere("_id=" + uri.getPathSegments().get(2)); 1975 // fall through 1976 case FILES: 1977 case MTP_OBJECTS: 1978 qb.setTables("files"); 1979 break; 1980 1981 case MTP_OBJECT_REFERENCES: 1982 int handle = Integer.parseInt(uri.getPathSegments().get(2)); 1983 return getObjectReferences(db, handle); 1984 1985 default: 1986 throw new IllegalStateException("Unknown URL: " + uri.toString()); 1987 } 1988 1989 // Log.v(TAG, "query = "+ qb.buildQuery(projectionIn, selection, selectionArgs, groupBy, null, sort, limit)); 1990 Cursor c = qb.query(db, projectionIn, selection, 1991 selectionArgs, groupBy, null, sort, limit); 1992 1993 if (c != null) { 1994 c.setNotificationUri(getContext().getContentResolver(), uri); 1995 } 1996 1997 return c; 1998 } 1999 2000 private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb, 2001 Uri uri, String[] projectionIn, String selection, 2002 String[] selectionArgs, String sort, int mode, 2003 String limit) { 2004 2005 String mSearchString = uri.getPath().endsWith("/") ? "" : uri.getLastPathSegment(); 2006 mSearchString = mSearchString.replaceAll(" ", " ").trim().toLowerCase(); 2007 2008 String [] searchWords = mSearchString.length() > 0 ? 2009 mSearchString.split(" ") : new String[0]; 2010 String [] wildcardWords = new String[searchWords.length]; 2011 Collator col = Collator.getInstance(); 2012 col.setStrength(Collator.PRIMARY); 2013 int len = searchWords.length; 2014 for (int i = 0; i < len; i++) { 2015 // Because we match on individual words here, we need to remove words 2016 // like 'a' and 'the' that aren't part of the keys. 2017 String key = MediaStore.Audio.keyFor(searchWords[i]); 2018 key = key.replace("\\", "\\\\"); 2019 key = key.replace("%", "\\%"); 2020 key = key.replace("_", "\\_"); 2021 wildcardWords[i] = 2022 (searchWords[i].equals("a") || searchWords[i].equals("an") || 2023 searchWords[i].equals("the")) ? "%" : "%" + key + "%"; 2024 } 2025 2026 String where = ""; 2027 for (int i = 0; i < searchWords.length; i++) { 2028 if (i == 0) { 2029 where = "match LIKE ? ESCAPE '\\'"; 2030 } else { 2031 where += " AND match LIKE ? ESCAPE '\\'"; 2032 } 2033 } 2034 2035 qb.setTables("search"); 2036 String [] cols; 2037 if (mode == AUDIO_SEARCH_FANCY) { 2038 cols = mSearchColsFancy; 2039 } else if (mode == AUDIO_SEARCH_BASIC) { 2040 cols = mSearchColsBasic; 2041 } else { 2042 cols = mSearchColsLegacy; 2043 } 2044 return qb.query(db, cols, where, wildcardWords, null, null, null, limit); 2045 } 2046 2047 @Override 2048 public String getType(Uri url) 2049 { 2050 switch (URI_MATCHER.match(url)) { 2051 case IMAGES_MEDIA_ID: 2052 case AUDIO_MEDIA_ID: 2053 case AUDIO_GENRES_ID_MEMBERS_ID: 2054 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 2055 case VIDEO_MEDIA_ID: 2056 case FILES_ID: 2057 Cursor c = null; 2058 try { 2059 c = query(url, MIME_TYPE_PROJECTION, null, null, null); 2060 if (c != null && c.getCount() == 1) { 2061 c.moveToFirst(); 2062 String mimeType = c.getString(1); 2063 c.deactivate(); 2064 return mimeType; 2065 } 2066 } finally { 2067 if (c != null) { 2068 c.close(); 2069 } 2070 } 2071 break; 2072 2073 case IMAGES_MEDIA: 2074 case IMAGES_THUMBNAILS: 2075 return Images.Media.CONTENT_TYPE; 2076 case AUDIO_ALBUMART_ID: 2077 case IMAGES_THUMBNAILS_ID: 2078 return "image/jpeg"; 2079 2080 case AUDIO_MEDIA: 2081 case AUDIO_GENRES_ID_MEMBERS: 2082 case AUDIO_PLAYLISTS_ID_MEMBERS: 2083 return Audio.Media.CONTENT_TYPE; 2084 2085 case AUDIO_GENRES: 2086 case AUDIO_MEDIA_ID_GENRES: 2087 return Audio.Genres.CONTENT_TYPE; 2088 case AUDIO_GENRES_ID: 2089 case AUDIO_MEDIA_ID_GENRES_ID: 2090 return Audio.Genres.ENTRY_CONTENT_TYPE; 2091 case AUDIO_PLAYLISTS: 2092 case AUDIO_MEDIA_ID_PLAYLISTS: 2093 return Audio.Playlists.CONTENT_TYPE; 2094 case AUDIO_PLAYLISTS_ID: 2095 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 2096 return Audio.Playlists.ENTRY_CONTENT_TYPE; 2097 2098 case VIDEO_MEDIA: 2099 return Video.Media.CONTENT_TYPE; 2100 } 2101 throw new IllegalStateException("Unknown URL : " + url); 2102 } 2103 2104 /** 2105 * Ensures there is a file in the _data column of values, if one isn't 2106 * present a new file is created. 2107 * 2108 * @param initialValues the values passed to insert by the caller 2109 * @return the new values 2110 */ 2111 private ContentValues ensureFile(boolean internal, ContentValues initialValues, 2112 String preferredExtension, String directoryName) { 2113 ContentValues values; 2114 String file = initialValues.getAsString("_data"); 2115 if (TextUtils.isEmpty(file)) { 2116 file = generateFileName(internal, preferredExtension, directoryName); 2117 values = new ContentValues(initialValues); 2118 values.put("_data", file); 2119 } else { 2120 values = initialValues; 2121 } 2122 2123 if (!ensureFileExists(file)) { 2124 throw new IllegalStateException("Unable to create new file: " + file); 2125 } 2126 return values; 2127 } 2128 2129 private void sendObjectAdded(long objectHandle) { 2130 IMtpService mtpService = mMtpService; 2131 if (mtpService != null) { 2132 try { 2133 mtpService.sendObjectAdded((int)objectHandle); 2134 } catch (RemoteException e) { 2135 Log.e(TAG, "RemoteException in sendObjectAdded", e); 2136 } 2137 } 2138 } 2139 2140 private void sendObjectRemoved(long objectHandle) { 2141 IMtpService mtpService = mMtpService; 2142 if (mtpService != null) { 2143 try { 2144 mtpService.sendObjectRemoved((int)objectHandle); 2145 } catch (RemoteException e) { 2146 Log.e(TAG, "RemoteException in sendObjectRemoved", e); 2147 } 2148 } 2149 } 2150 2151 @Override 2152 public int bulkInsert(Uri uri, ContentValues values[]) { 2153 int match = URI_MATCHER.match(uri); 2154 if (match == VOLUMES) { 2155 return super.bulkInsert(uri, values); 2156 } 2157 DatabaseHelper database = getDatabaseForUri(uri); 2158 if (database == null) { 2159 throw new UnsupportedOperationException( 2160 "Unknown URI: " + uri); 2161 } 2162 SQLiteDatabase db = database.getWritableDatabase(); 2163 if (db == null) { 2164 throw new IllegalStateException("Couldn't open database for " + uri); 2165 } 2166 2167 if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) { 2168 return playlistBulkInsert(db, uri, values); 2169 } else if (match == MTP_OBJECT_REFERENCES) { 2170 int handle = Integer.parseInt(uri.getPathSegments().get(2)); 2171 return setObjectReferences(db, handle, values); 2172 } 2173 2174 db.beginTransaction(); 2175 int numInserted = 0; 2176 try { 2177 int len = values.length; 2178 for (int i = 0; i < len; i++) { 2179 insertInternal(uri, match, values[i]); 2180 } 2181 numInserted = len; 2182 db.setTransactionSuccessful(); 2183 } finally { 2184 db.endTransaction(); 2185 } 2186 getContext().getContentResolver().notifyChange(uri, null); 2187 return numInserted; 2188 } 2189 2190 @Override 2191 public Uri insert(Uri uri, ContentValues initialValues) 2192 { 2193 int match = URI_MATCHER.match(uri); 2194 Uri newUri = insertInternal(uri, match, initialValues); 2195 // do not signal notification for MTP objects. 2196 // we will signal instead after file transfer is successful. 2197 if (newUri != null && match != MTP_OBJECTS) { 2198 getContext().getContentResolver().notifyChange(uri, null); 2199 } 2200 return newUri; 2201 } 2202 2203 private int playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[]) { 2204 DatabaseUtils.InsertHelper helper = 2205 new DatabaseUtils.InsertHelper(db, "audio_playlists_map"); 2206 int audioidcolidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID); 2207 int playlistididx = helper.getColumnIndex(Audio.Playlists.Members.PLAYLIST_ID); 2208 int playorderidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 2209 long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 2210 2211 db.beginTransaction(); 2212 int numInserted = 0; 2213 try { 2214 int len = values.length; 2215 for (int i = 0; i < len; i++) { 2216 helper.prepareForInsert(); 2217 // getting the raw Object and converting it long ourselves saves 2218 // an allocation (the alternative is ContentValues.getAsLong, which 2219 // returns a Long object) 2220 long audioid = ((Number) values[i].get( 2221 MediaStore.Audio.Playlists.Members.AUDIO_ID)).longValue(); 2222 helper.bind(audioidcolidx, audioid); 2223 helper.bind(playlistididx, playlistId); 2224 // convert to int ourselves to save an allocation. 2225 int playorder = ((Number) values[i].get( 2226 MediaStore.Audio.Playlists.Members.PLAY_ORDER)).intValue(); 2227 helper.bind(playorderidx, playorder); 2228 helper.execute(); 2229 } 2230 numInserted = len; 2231 db.setTransactionSuccessful(); 2232 } finally { 2233 db.endTransaction(); 2234 helper.close(); 2235 } 2236 getContext().getContentResolver().notifyChange(uri, null); 2237 return numInserted; 2238 } 2239 2240 private long insertDirectory(SQLiteDatabase db, String path) { 2241 ContentValues values = new ContentValues(); 2242 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 2243 values.put(FileColumns.DATA, path); 2244 values.put(FileColumns.PARENT, getParent(db, path)); 2245 long rowId = db.insert("files", FileColumns.DATE_MODIFIED, values); 2246 sendObjectAdded(rowId); 2247 return rowId; 2248 } 2249 2250 private long getParent(SQLiteDatabase db, String path) { 2251 int lastSlash = path.lastIndexOf('/'); 2252 if (lastSlash > 0) { 2253 String parentPath = path.substring(0, lastSlash); 2254 if (parentPath.equals(mExternalStoragePath)) { 2255 return 0; 2256 } 2257 // Use "LIKE" instead of "=" on case insensitive file systems so we do a 2258 // case insensitive match when looking for parent directory. 2259 String selection = (mCaseInsensitivePaths ? MediaStore.MediaColumns.DATA + " LIKE ?" 2260 : MediaStore.MediaColumns.DATA + "=?"); 2261 String [] selargs = { parentPath }; 2262 Cursor c = db.query("files", null, selection, selargs, null, null, null); 2263 try { 2264 if (c == null || c.getCount() == 0) { 2265 // parent isn't in the database - so add it 2266 return insertDirectory(db, parentPath); 2267 } else { 2268 c.moveToFirst(); 2269 return c.getLong(0); 2270 } 2271 } finally { 2272 if (c != null) c.close(); 2273 } 2274 } else { 2275 return 0; 2276 } 2277 } 2278 2279 private long insertFile(DatabaseHelper database, Uri uri, ContentValues initialValues, int mediaType, 2280 boolean notify) { 2281 SQLiteDatabase db = database.getWritableDatabase(); 2282 ContentValues values = null; 2283 2284 switch (mediaType) { 2285 case FileColumns.MEDIA_TYPE_IMAGE: { 2286 values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/Camera"); 2287 2288 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 2289 String data = values.getAsString(MediaColumns.DATA); 2290 if (! values.containsKey(MediaColumns.DISPLAY_NAME)) { 2291 computeDisplayName(data, values); 2292 } 2293 computeTakenTime(values); 2294 break; 2295 } 2296 2297 case FileColumns.MEDIA_TYPE_AUDIO: { 2298 // SQLite Views are read-only, so we need to deconstruct this 2299 // insert and do inserts into the underlying tables. 2300 // If doing this here turns out to be a performance bottleneck, 2301 // consider moving this to native code and using triggers on 2302 // the view. 2303 values = new ContentValues(initialValues); 2304 2305 // Insert the artist into the artist table and remove it from 2306 // the input values 2307 Object so = values.get("artist"); 2308 String s = (so == null ? "" : so.toString()); 2309 values.remove("artist"); 2310 long artistRowId; 2311 HashMap<String, Long> artistCache = database.mArtistCache; 2312 String path = values.getAsString("_data"); 2313 synchronized(artistCache) { 2314 Long temp = artistCache.get(s); 2315 if (temp == null) { 2316 artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist", 2317 s, s, path, 0, null, artistCache, uri); 2318 } else { 2319 artistRowId = temp.longValue(); 2320 } 2321 } 2322 String artist = s; 2323 2324 // Do the same for the album field 2325 so = values.get("album"); 2326 s = (so == null ? "" : so.toString()); 2327 values.remove("album"); 2328 long albumRowId; 2329 HashMap<String, Long> albumCache = database.mAlbumCache; 2330 synchronized(albumCache) { 2331 int albumhash = path.substring(0, path.lastIndexOf('/')).hashCode(); 2332 String cacheName = s + albumhash; 2333 Long temp = albumCache.get(cacheName); 2334 if (temp == null) { 2335 albumRowId = getKeyIdForName(db, "albums", "album_key", "album", 2336 s, cacheName, path, albumhash, artist, albumCache, uri); 2337 } else { 2338 albumRowId = temp; 2339 } 2340 } 2341 2342 values.put("artist_id", Integer.toString((int)artistRowId)); 2343 values.put("album_id", Integer.toString((int)albumRowId)); 2344 so = values.getAsString("title"); 2345 s = (so == null ? "" : so.toString()); 2346 values.put("title_key", MediaStore.Audio.keyFor(s)); 2347 // do a final trim of the title, in case it started with the special 2348 // "sort first" character (ascii \001) 2349 values.remove("title"); 2350 values.put("title", s.trim()); 2351 2352 computeDisplayName(values.getAsString("_data"), values); 2353 break; 2354 } 2355 2356 case FileColumns.MEDIA_TYPE_VIDEO: { 2357 values = ensureFile(database.mInternal, initialValues, ".3gp", "video"); 2358 String data = values.getAsString("_data"); 2359 computeDisplayName(data, values); 2360 computeTakenTime(values); 2361 break; 2362 } 2363 } 2364 2365 if (values == null) { 2366 values = new ContentValues(initialValues); 2367 } 2368 // compute bucket_id and bucket_display_name for all files 2369 String path = values.getAsString(MediaStore.MediaColumns.DATA); 2370 if (path != null) { 2371 computeBucketValues(path, values); 2372 } 2373 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 2374 2375 long rowId = 0; 2376 Integer i = values.getAsInteger( 2377 MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID); 2378 if (i != null) { 2379 rowId = i.intValue(); 2380 values = new ContentValues(values); 2381 values.remove(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID); 2382 } 2383 2384 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 2385 if (title == null && path != null) { 2386 title = MediaFile.getFileTitle(path); 2387 } 2388 values.put(FileColumns.TITLE, title); 2389 2390 String mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE); 2391 Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 2392 int format = (formatObject == null ? 0 : formatObject.intValue()); 2393 if (format == 0) { 2394 if (path == null) { 2395 // special case device created playlists 2396 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 2397 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST); 2398 // create a file path for the benefit of MTP 2399 path = mExternalStoragePath 2400 + "/Playlists/" + values.getAsString(Audio.Playlists.NAME); 2401 values.put(MediaStore.MediaColumns.DATA, path); 2402 values.put(FileColumns.PARENT, getParent(db, path)); 2403 } else { 2404 Log.e(TAG, "path is null in insertObject()"); 2405 } 2406 } else { 2407 format = MediaFile.getFormatCode(path, mimeType); 2408 } 2409 } 2410 if (format != 0) { 2411 values.put(FileColumns.FORMAT, format); 2412 if (mimeType == null) { 2413 mimeType = MediaFile.getMimeTypeForFormatCode(format); 2414 } 2415 } 2416 2417 if (mimeType == null) { 2418 mimeType = MediaFile.getMimeTypeForFile(path); 2419 } 2420 if (mimeType != null) { 2421 values.put(FileColumns.MIME_TYPE, mimeType); 2422 2423 if (mediaType == FileColumns.MEDIA_TYPE_NONE) { 2424 int fileType = MediaFile.getFileTypeForMimeType(mimeType); 2425 if (MediaFile.isAudioFileType(fileType)) { 2426 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 2427 } else if (MediaFile.isVideoFileType(fileType)) { 2428 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 2429 } else if (MediaFile.isImageFileType(fileType)) { 2430 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 2431 } else if (MediaFile.isPlayListFileType(fileType)) { 2432 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 2433 } 2434 } 2435 } 2436 values.put(FileColumns.MEDIA_TYPE, mediaType); 2437 2438 if (rowId == 0) { 2439 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 2440 String name = values.getAsString(Audio.Playlists.NAME); 2441 if (name == null && path == null) { 2442 // MediaScanner will compute the name from the path if we have one 2443 throw new IllegalArgumentException( 2444 "no name was provided when inserting abstract playlist"); 2445 } 2446 } else { 2447 if (path == null) { 2448 // path might be null for playlists created on the device 2449 // or transfered via MTP 2450 throw new IllegalArgumentException( 2451 "no path was provided when inserting new file"); 2452 } 2453 } 2454 2455 Long size = values.getAsLong(MediaStore.MediaColumns.SIZE); 2456 if (size == null) { 2457 if (path != null) { 2458 File file = new File(path); 2459 values.put(FileColumns.SIZE, file.length()); 2460 } 2461 } else { 2462 values.put(FileColumns.SIZE, size); 2463 } 2464 2465 Long parent = values.getAsLong(FileColumns.PARENT); 2466 if (parent == null) { 2467 if (path != null) { 2468 long parentId = getParent(db, path); 2469 values.put(FileColumns.PARENT, parentId); 2470 } 2471 } else { 2472 values.put(FileColumns.PARENT, parent); 2473 } 2474 2475 rowId = db.insert("files", FileColumns.DATE_MODIFIED, values); 2476 if (LOCAL_LOGV) Log.v(TAG, "insertFile: values=" + values + " returned: " + rowId); 2477 2478 if (rowId != 0 && notify) { 2479 sendObjectAdded(rowId); 2480 } 2481 } else { 2482 db.update("files", values, FileColumns._ID + "=?", 2483 new String[] { Long.toString(rowId) }); 2484 } 2485 2486 return rowId; 2487 } 2488 2489 private Cursor getObjectReferences(SQLiteDatabase db, int handle) { 2490 Cursor c = db.query("files", mMediaTableColumns, "_id=?", 2491 new String[] { Integer.toString(handle) }, 2492 null, null, null); 2493 try { 2494 if (c != null && c.moveToNext()) { 2495 long playlistId = c.getLong(0); 2496 int mediaType = c.getInt(1); 2497 if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) { 2498 // we only support object references for playlist objects 2499 return null; 2500 } 2501 return db.rawQuery(OBJECT_REFERENCES_QUERY, 2502 new String[] { Long.toString(playlistId) } ); 2503 } 2504 } finally { 2505 if (c != null) { 2506 c.close(); 2507 } 2508 } 2509 return null; 2510 } 2511 2512 private int setObjectReferences(SQLiteDatabase db, int handle, ContentValues values[]) { 2513 // first look up the media table and media ID for the object 2514 long playlistId = 0; 2515 Cursor c = db.query("files", mMediaTableColumns, "_id=?", 2516 new String[] { Integer.toString(handle) }, 2517 null, null, null); 2518 try { 2519 if (c != null && c.moveToNext()) { 2520 int mediaType = c.getInt(1); 2521 if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) { 2522 // we only support object references for playlist objects 2523 return 0; 2524 } 2525 playlistId = c.getLong(0); 2526 } 2527 } finally { 2528 if (c != null) { 2529 c.close(); 2530 } 2531 } 2532 if (playlistId == 0) { 2533 return 0; 2534 } 2535 2536 // next delete any existing entries 2537 db.delete("audio_playlists_map", "playlist_id=?", 2538 new String[] { Long.toString(playlistId) }); 2539 2540 // finally add the new entries 2541 int count = values.length; 2542 int added = 0; 2543 ContentValues[] valuesList = new ContentValues[count]; 2544 for (int i = 0; i < count; i++) { 2545 // convert object ID to audio ID 2546 long audioId = 0; 2547 long objectId = values[i].getAsLong(MediaStore.MediaColumns._ID); 2548 c = db.query("files", mMediaTableColumns, "_id=?", 2549 new String[] { Long.toString(objectId) }, 2550 null, null, null); 2551 try { 2552 if (c != null && c.moveToNext()) { 2553 int mediaType = c.getInt(1); 2554 if (mediaType != FileColumns.MEDIA_TYPE_AUDIO) { 2555 // we only allow audio files in playlists, so skip 2556 continue; 2557 } 2558 audioId = c.getLong(0); 2559 } 2560 } finally { 2561 if (c != null) { 2562 c.close(); 2563 } 2564 } 2565 if (audioId != 0) { 2566 ContentValues v = new ContentValues(); 2567 v.put(MediaStore.Audio.Playlists.Members.PLAYLIST_ID, playlistId); 2568 v.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId); 2569 v.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, added); 2570 valuesList[added++] = v; 2571 } 2572 } 2573 if (added < count) { 2574 // we weren't able to find everything on the list, so lets resize the array 2575 // and pass what we have. 2576 ContentValues[] newValues = new ContentValues[added]; 2577 System.arraycopy(valuesList, 0, newValues, 0, added); 2578 valuesList = newValues; 2579 } 2580 return playlistBulkInsert(db, 2581 Audio.Playlists.Members.getContentUri(EXTERNAL_VOLUME, playlistId), 2582 valuesList); 2583 } 2584 2585 private Uri insertInternal(Uri uri, int match, ContentValues initialValues) { 2586 long rowId; 2587 2588 if (LOCAL_LOGV) Log.v(TAG, "insertInternal: "+uri+", initValues="+initialValues); 2589 // handle MEDIA_SCANNER before calling getDatabaseForUri() 2590 if (match == MEDIA_SCANNER) { 2591 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 2592 return MediaStore.getMediaScannerUri(); 2593 } 2594 2595 Uri newUri = null; 2596 DatabaseHelper database = getDatabaseForUri(uri); 2597 if (database == null && match != VOLUMES) { 2598 throw new UnsupportedOperationException( 2599 "Unknown URI: " + uri); 2600 } 2601 SQLiteDatabase db = (match == VOLUMES ? null : database.getWritableDatabase()); 2602 2603 switch (match) { 2604 case IMAGES_MEDIA: { 2605 rowId = insertFile(database, uri, initialValues, FileColumns.MEDIA_TYPE_IMAGE, true); 2606 if (rowId > 0) { 2607 newUri = ContentUris.withAppendedId( 2608 Images.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 2609 } 2610 break; 2611 } 2612 2613 // This will be triggered by requestMediaThumbnail (see getThumbnailUri) 2614 case IMAGES_THUMBNAILS: { 2615 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", 2616 "DCIM/.thumbnails"); 2617 rowId = db.insert("thumbnails", "name", values); 2618 if (rowId > 0) { 2619 newUri = ContentUris.withAppendedId(Images.Thumbnails. 2620 getContentUri(uri.getPathSegments().get(0)), rowId); 2621 } 2622 break; 2623 } 2624 2625 // This is currently only used by MICRO_KIND video thumbnail (see getThumbnailUri) 2626 case VIDEO_THUMBNAILS: { 2627 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", 2628 "DCIM/.thumbnails"); 2629 rowId = db.insert("videothumbnails", "name", values); 2630 if (rowId > 0) { 2631 newUri = ContentUris.withAppendedId(Video.Thumbnails. 2632 getContentUri(uri.getPathSegments().get(0)), rowId); 2633 } 2634 break; 2635 } 2636 2637 case AUDIO_MEDIA: { 2638 rowId = insertFile(database, uri, initialValues, FileColumns.MEDIA_TYPE_AUDIO, true); 2639 if (rowId > 0) { 2640 newUri = ContentUris.withAppendedId(Audio.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 2641 } 2642 break; 2643 } 2644 2645 case AUDIO_MEDIA_ID_GENRES: { 2646 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 2647 ContentValues values = new ContentValues(initialValues); 2648 values.put(Audio.Genres.Members.AUDIO_ID, audioId); 2649 rowId = db.insert("audio_genres_map", "genre_id", values); 2650 if (rowId > 0) { 2651 newUri = ContentUris.withAppendedId(uri, rowId); 2652 } 2653 break; 2654 } 2655 2656 case AUDIO_MEDIA_ID_PLAYLISTS: { 2657 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 2658 ContentValues values = new ContentValues(initialValues); 2659 values.put(Audio.Playlists.Members.AUDIO_ID, audioId); 2660 rowId = db.insert("audio_playlists_map", "playlist_id", 2661 values); 2662 if (rowId > 0) { 2663 newUri = ContentUris.withAppendedId(uri, rowId); 2664 } 2665 break; 2666 } 2667 2668 case AUDIO_GENRES: { 2669 rowId = db.insert("audio_genres", "audio_id", initialValues); 2670 if (rowId > 0) { 2671 newUri = ContentUris.withAppendedId(Audio.Genres.getContentUri(uri.getPathSegments().get(0)), rowId); 2672 } 2673 break; 2674 } 2675 2676 case AUDIO_GENRES_ID_MEMBERS: { 2677 Long genreId = Long.parseLong(uri.getPathSegments().get(3)); 2678 ContentValues values = new ContentValues(initialValues); 2679 values.put(Audio.Genres.Members.GENRE_ID, genreId); 2680 rowId = db.insert("audio_genres_map", "genre_id", values); 2681 if (rowId > 0) { 2682 newUri = ContentUris.withAppendedId(uri, rowId); 2683 } 2684 break; 2685 } 2686 2687 case AUDIO_PLAYLISTS: { 2688 ContentValues values = new ContentValues(initialValues); 2689 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 2690 rowId = insertFile(database, uri, values, FileColumns.MEDIA_TYPE_PLAYLIST, true); 2691 if (rowId > 0) { 2692 newUri = ContentUris.withAppendedId(Audio.Playlists.getContentUri(uri.getPathSegments().get(0)), rowId); 2693 } 2694 break; 2695 } 2696 2697 case AUDIO_PLAYLISTS_ID: 2698 case AUDIO_PLAYLISTS_ID_MEMBERS: { 2699 Long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 2700 ContentValues values = new ContentValues(initialValues); 2701 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId); 2702 rowId = db.insert("audio_playlists_map", "playlist_id", values); 2703 if (rowId > 0) { 2704 newUri = ContentUris.withAppendedId(uri, rowId); 2705 } 2706 break; 2707 } 2708 2709 case VIDEO_MEDIA: { 2710 rowId = insertFile(database, uri, initialValues, FileColumns.MEDIA_TYPE_VIDEO, true); 2711 if (rowId > 0) { 2712 newUri = ContentUris.withAppendedId(Video.Media.getContentUri( 2713 uri.getPathSegments().get(0)), rowId); 2714 } 2715 break; 2716 } 2717 2718 case AUDIO_ALBUMART: { 2719 if (database.mInternal) { 2720 throw new UnsupportedOperationException("no internal album art allowed"); 2721 } 2722 ContentValues values = null; 2723 try { 2724 values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 2725 } catch (IllegalStateException ex) { 2726 // probably no more room to store albumthumbs 2727 values = initialValues; 2728 } 2729 rowId = db.insert("album_art", "_data", values); 2730 if (rowId > 0) { 2731 newUri = ContentUris.withAppendedId(uri, rowId); 2732 } 2733 break; 2734 } 2735 2736 case VOLUMES: 2737 return attachVolume(initialValues.getAsString("name")); 2738 2739 case FILES: 2740 rowId = insertFile(database, uri, initialValues, 2741 FileColumns.MEDIA_TYPE_NONE, true); 2742 if (rowId > 0) { 2743 newUri = Files.getContentUri(uri.getPathSegments().get(0), rowId); 2744 } 2745 break; 2746 2747 case MTP_OBJECTS: 2748 // don't send a notification if the insert originated from MTP 2749 rowId = insertFile(database, uri, initialValues, 2750 FileColumns.MEDIA_TYPE_NONE, false); 2751 if (rowId > 0) { 2752 newUri = Files.getMtpObjectsUri(uri.getPathSegments().get(0), rowId); 2753 } 2754 break; 2755 2756 default: 2757 throw new UnsupportedOperationException("Invalid URI " + uri); 2758 } 2759 2760 return newUri; 2761 } 2762 2763 @Override 2764 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 2765 throws OperationApplicationException { 2766 2767 // The operations array provides no overall information about the URI(s) being operated 2768 // on, so begin a transaction for ALL of the databases. 2769 DatabaseHelper ihelper = getDatabaseForUri(MediaStore.Audio.Media.INTERNAL_CONTENT_URI); 2770 DatabaseHelper ehelper = getDatabaseForUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI); 2771 SQLiteDatabase idb = ihelper.getWritableDatabase(); 2772 idb.beginTransaction(); 2773 SQLiteDatabase edb = null; 2774 if (ehelper != null) { 2775 edb = ehelper.getWritableDatabase(); 2776 edb.beginTransaction(); 2777 } 2778 try { 2779 ContentProviderResult[] result = super.applyBatch(operations); 2780 idb.setTransactionSuccessful(); 2781 if (edb != null) { 2782 edb.setTransactionSuccessful(); 2783 } 2784 // Rather than sending targeted change notifications for every Uri 2785 // affected by the batch operation, just invalidate the entire internal 2786 // and external name space. 2787 ContentResolver res = getContext().getContentResolver(); 2788 res.notifyChange(Uri.parse("content://media/"), null); 2789 return result; 2790 } finally { 2791 idb.endTransaction(); 2792 if (edb != null) { 2793 edb.endTransaction(); 2794 } 2795 } 2796 } 2797 2798 2799 private MediaThumbRequest requestMediaThumbnail(String path, Uri uri, int priority, long magic) { 2800 synchronized (mMediaThumbQueue) { 2801 MediaThumbRequest req = null; 2802 try { 2803 req = new MediaThumbRequest( 2804 getContext().getContentResolver(), path, uri, priority, magic); 2805 mMediaThumbQueue.add(req); 2806 // Trigger the handler. 2807 Message msg = mThumbHandler.obtainMessage(IMAGE_THUMB); 2808 msg.sendToTarget(); 2809 } catch (Throwable t) { 2810 Log.w(TAG, t); 2811 } 2812 return req; 2813 } 2814 } 2815 2816 private String generateFileName(boolean internal, String preferredExtension, String directoryName) 2817 { 2818 // create a random file 2819 String name = String.valueOf(System.currentTimeMillis()); 2820 2821 if (internal) { 2822 throw new UnsupportedOperationException("Writing to internal storage is not supported."); 2823// return Environment.getDataDirectory() 2824// + "/" + directoryName + "/" + name + preferredExtension; 2825 } else { 2826 return mExternalStoragePath + "/" + directoryName + "/" + name + preferredExtension; 2827 } 2828 } 2829 2830 private boolean ensureFileExists(String path) { 2831 File file = new File(path); 2832 if (file.exists()) { 2833 return true; 2834 } else { 2835 // we will not attempt to create the first directory in the path 2836 // (for example, do not create /sdcard if the SD card is not mounted) 2837 int secondSlash = path.indexOf('/', 1); 2838 if (secondSlash < 1) return false; 2839 String directoryPath = path.substring(0, secondSlash); 2840 File directory = new File(directoryPath); 2841 if (!directory.exists()) 2842 return false; 2843 file.getParentFile().mkdirs(); 2844 try { 2845 return file.createNewFile(); 2846 } catch(IOException ioe) { 2847 Log.e(TAG, "File creation failed", ioe); 2848 } 2849 return false; 2850 } 2851 } 2852 2853 private static final class GetTableAndWhereOutParameter { 2854 public String table; 2855 public String where; 2856 } 2857 2858 static final GetTableAndWhereOutParameter sGetTableAndWhereParam = 2859 new GetTableAndWhereOutParameter(); 2860 2861 private void getTableAndWhere(Uri uri, int match, String userWhere, 2862 GetTableAndWhereOutParameter out) { 2863 String where = null; 2864 switch (match) { 2865 case IMAGES_MEDIA: 2866 out.table = "files"; 2867 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_IMAGE; 2868 break; 2869 2870 case IMAGES_MEDIA_ID: 2871 out.table = "files"; 2872 where = "_id = " + uri.getPathSegments().get(3); 2873 break; 2874 2875 case IMAGES_THUMBNAILS_ID: 2876 where = "_id=" + uri.getPathSegments().get(3); 2877 case IMAGES_THUMBNAILS: 2878 out.table = "thumbnails"; 2879 break; 2880 2881 case AUDIO_MEDIA: 2882 out.table = "files"; 2883 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_AUDIO; 2884 break; 2885 2886 case AUDIO_MEDIA_ID: 2887 out.table = "files"; 2888 where = "_id=" + uri.getPathSegments().get(3); 2889 break; 2890 2891 case AUDIO_MEDIA_ID_GENRES: 2892 out.table = "audio_genres"; 2893 where = "audio_id=" + uri.getPathSegments().get(3); 2894 break; 2895 2896 case AUDIO_MEDIA_ID_GENRES_ID: 2897 out.table = "audio_genres"; 2898 where = "audio_id=" + uri.getPathSegments().get(3) + 2899 " AND genre_id=" + uri.getPathSegments().get(5); 2900 break; 2901 2902 case AUDIO_MEDIA_ID_PLAYLISTS: 2903 out.table = "audio_playlists"; 2904 where = "audio_id=" + uri.getPathSegments().get(3); 2905 break; 2906 2907 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 2908 out.table = "audio_playlists"; 2909 where = "audio_id=" + uri.getPathSegments().get(3) + 2910 " AND playlists_id=" + uri.getPathSegments().get(5); 2911 break; 2912 2913 case AUDIO_GENRES: 2914 out.table = "audio_genres"; 2915 break; 2916 2917 case AUDIO_GENRES_ID: 2918 out.table = "audio_genres"; 2919 where = "_id=" + uri.getPathSegments().get(3); 2920 break; 2921 2922 case AUDIO_GENRES_ID_MEMBERS: 2923 out.table = "audio_genres"; 2924 where = "genre_id=" + uri.getPathSegments().get(3); 2925 break; 2926 2927 case AUDIO_GENRES_ID_MEMBERS_ID: 2928 out.table = "audio_genres"; 2929 where = "genre_id=" + uri.getPathSegments().get(3) + 2930 " AND audio_id =" + uri.getPathSegments().get(5); 2931 break; 2932 2933 case AUDIO_PLAYLISTS: 2934 out.table = "files"; 2935 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_PLAYLIST; 2936 break; 2937 2938 case AUDIO_PLAYLISTS_ID: 2939 out.table = "files"; 2940 where = "_id=" + uri.getPathSegments().get(3); 2941 break; 2942 2943 case AUDIO_PLAYLISTS_ID_MEMBERS: 2944 out.table = "audio_playlists_map"; 2945 where = "playlist_id=" + uri.getPathSegments().get(3); 2946 break; 2947 2948 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 2949 out.table = "audio_playlists_map"; 2950 where = "playlist_id=" + uri.getPathSegments().get(3) + 2951 " AND _id=" + uri.getPathSegments().get(5); 2952 break; 2953 2954 case AUDIO_ALBUMART_ID: 2955 out.table = "album_art"; 2956 where = "album_id=" + uri.getPathSegments().get(3); 2957 break; 2958 2959 case VIDEO_MEDIA: 2960 out.table = "files"; 2961 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_VIDEO; 2962 break; 2963 2964 case VIDEO_MEDIA_ID: 2965 out.table = "files"; 2966 where = "_id=" + uri.getPathSegments().get(3); 2967 break; 2968 2969 case VIDEO_THUMBNAILS_ID: 2970 where = "_id=" + uri.getPathSegments().get(3); 2971 case VIDEO_THUMBNAILS: 2972 out.table = "videothumbnails"; 2973 break; 2974 2975 case FILES_ID: 2976 case MTP_OBJECTS_ID: 2977 where = "_id=" + uri.getPathSegments().get(2); 2978 case FILES: 2979 case MTP_OBJECTS: 2980 out.table = "files"; 2981 break; 2982 2983 default: 2984 throw new UnsupportedOperationException( 2985 "Unknown or unsupported URL: " + uri.toString()); 2986 } 2987 2988 // Add in the user requested WHERE clause, if needed 2989 if (!TextUtils.isEmpty(userWhere)) { 2990 if (!TextUtils.isEmpty(where)) { 2991 out.where = where + " AND (" + userWhere + ")"; 2992 } else { 2993 out.where = userWhere; 2994 } 2995 } else { 2996 out.where = where; 2997 } 2998 } 2999 3000 @Override 3001 public int delete(Uri uri, String userWhere, String[] whereArgs) { 3002 int count; 3003 int match = URI_MATCHER.match(uri); 3004 3005 // handle MEDIA_SCANNER before calling getDatabaseForUri() 3006 if (match == MEDIA_SCANNER) { 3007 if (mMediaScannerVolume == null) { 3008 return 0; 3009 } 3010 mMediaScannerVolume = null; 3011 return 1; 3012 } 3013 3014 if (match != VOLUMES_ID) { 3015 DatabaseHelper database = getDatabaseForUri(uri); 3016 if (database == null) { 3017 throw new UnsupportedOperationException( 3018 "Unknown URI: " + uri); 3019 } 3020 SQLiteDatabase db = database.getWritableDatabase(); 3021 3022 synchronized (sGetTableAndWhereParam) { 3023 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 3024 switch (match) { 3025 case MTP_OBJECTS: 3026 case MTP_OBJECTS_ID: 3027 try { 3028 // don't send objectRemoved event since this originated from MTP 3029 mDisableMtpObjectCallbacks = true; 3030 count = db.delete("files", sGetTableAndWhereParam.where, whereArgs); 3031 } finally { 3032 mDisableMtpObjectCallbacks = false; 3033 } 3034 break; 3035 3036 default: 3037 count = db.delete(sGetTableAndWhereParam.table, 3038 sGetTableAndWhereParam.where, whereArgs); 3039 break; 3040 } 3041 // Since there are multiple Uris that can refer to the same files 3042 // and deletes can affect other objects in storage (like subdirectories 3043 // or playlists) we will notify a change on the entire volume to make 3044 // sure no listeners miss the notification. 3045 String volume = uri.getPathSegments().get(0); 3046 Uri notifyUri = Uri.parse("content://" + MediaStore.AUTHORITY + "/" + volume); 3047 getContext().getContentResolver().notifyChange(notifyUri, null); 3048 } 3049 } else { 3050 detachVolume(uri); 3051 count = 1; 3052 } 3053 3054 return count; 3055 } 3056 3057 @Override 3058 public int update(Uri uri, ContentValues initialValues, String userWhere, 3059 String[] whereArgs) { 3060 int count; 3061 // Log.v(TAG, "update for uri="+uri+", initValues="+initialValues); 3062 int match = URI_MATCHER.match(uri); 3063 DatabaseHelper database = getDatabaseForUri(uri); 3064 if (database == null) { 3065 throw new UnsupportedOperationException( 3066 "Unknown URI: " + uri); 3067 } 3068 SQLiteDatabase db = database.getWritableDatabase(); 3069 3070 synchronized (sGetTableAndWhereParam) { 3071 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 3072 3073 // special case renaming directories via MTP. 3074 // in this case we must update all paths in the database with 3075 // the directory name as a prefix 3076 if ((match == MTP_OBJECTS || match == MTP_OBJECTS_ID) 3077 && initialValues != null && initialValues.size() == 1) { 3078 String oldPath = null; 3079 String newPath = initialValues.getAsString("_data"); 3080 // MtpDatabase will rename the directory first, so we test the new file name 3081 if (newPath != null && (new File(newPath)).isDirectory()) { 3082 Cursor cursor = db.query(sGetTableAndWhereParam.table, PATH_PROJECTION, 3083 userWhere, whereArgs, null, null, null); 3084 try { 3085 if (cursor != null && cursor.moveToNext()) { 3086 oldPath = cursor.getString(1); 3087 } 3088 } finally { 3089 if (cursor != null) cursor.close(); 3090 } 3091 if (oldPath != null) { 3092 // first rename the row for the directory 3093 count = db.update(sGetTableAndWhereParam.table, initialValues, 3094 sGetTableAndWhereParam.where, whereArgs); 3095 if (count > 0) { 3096 // then update the paths of any files and folders contained in the directory. 3097 db.execSQL("UPDATE files SET _data=REPLACE(_data, '" 3098 + oldPath + "/','" + newPath + "/');"); 3099 } 3100 3101 if (count > 0 && !db.inTransaction()) { 3102 getContext().getContentResolver().notifyChange(uri, null); 3103 } 3104 return count; 3105 } 3106 } 3107 } 3108 3109 switch (match) { 3110 case AUDIO_MEDIA: 3111 case AUDIO_MEDIA_ID: 3112 { 3113 ContentValues values = new ContentValues(initialValues); 3114 3115 // Insert the artist into the artist table and remove it from 3116 // the input values 3117 String artist = values.getAsString("artist"); 3118 values.remove("artist"); 3119 if (artist != null) { 3120 long artistRowId; 3121 HashMap<String, Long> artistCache = database.mArtistCache; 3122 synchronized(artistCache) { 3123 Long temp = artistCache.get(artist); 3124 if (temp == null) { 3125 artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist", 3126 artist, artist, null, 0, null, artistCache, uri); 3127 } else { 3128 artistRowId = temp.longValue(); 3129 } 3130 } 3131 values.put("artist_id", Integer.toString((int)artistRowId)); 3132 } 3133 3134 // Do the same for the album field. 3135 String so = values.getAsString("album"); 3136 values.remove("album"); 3137 if (so != null) { 3138 String path = values.getAsString("_data"); 3139 int albumHash = 0; 3140 if (path == null) { 3141 // If the path is null, we don't have a hash for the file in question. 3142 Log.w(TAG, "Update without specified path."); 3143 } else { 3144 albumHash = path.substring(0, path.lastIndexOf('/')).hashCode(); 3145 } 3146 String s = so.toString(); 3147 long albumRowId; 3148 HashMap<String, Long> albumCache = database.mAlbumCache; 3149 synchronized(albumCache) { 3150 String cacheName = s + albumHash; 3151 Long temp = albumCache.get(cacheName); 3152 if (temp == null) { 3153 albumRowId = getKeyIdForName(db, "albums", "album_key", "album", 3154 s, cacheName, path, albumHash, artist, albumCache, uri); 3155 } else { 3156 albumRowId = temp.longValue(); 3157 } 3158 } 3159 values.put("album_id", Integer.toString((int)albumRowId)); 3160 } 3161 3162 // don't allow the title_key field to be updated directly 3163 values.remove("title_key"); 3164 // If the title field is modified, update the title_key 3165 so = values.getAsString("title"); 3166 if (so != null) { 3167 String s = so.toString(); 3168 values.put("title_key", MediaStore.Audio.keyFor(s)); 3169 // do a final trim of the title, in case it started with the special 3170 // "sort first" character (ascii \001) 3171 values.remove("title"); 3172 values.put("title", s.trim()); 3173 } 3174 3175 count = db.update(sGetTableAndWhereParam.table, values, 3176 sGetTableAndWhereParam.where, whereArgs); 3177 } 3178 break; 3179 case IMAGES_MEDIA: 3180 case IMAGES_MEDIA_ID: 3181 case VIDEO_MEDIA: 3182 case VIDEO_MEDIA_ID: 3183 { 3184 ContentValues values = new ContentValues(initialValues); 3185 // Don't allow bucket id or display name to be updated directly. 3186 // The same names are used for both images and table columns, so 3187 // we use the ImageColumns constants here. 3188 values.remove(ImageColumns.BUCKET_ID); 3189 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 3190 // If the data is being modified update the bucket values 3191 String data = values.getAsString(MediaColumns.DATA); 3192 if (data != null) { 3193 computeBucketValues(data, values); 3194 } 3195 computeTakenTime(values); 3196 count = db.update(sGetTableAndWhereParam.table, values, 3197 sGetTableAndWhereParam.where, whereArgs); 3198 // if this is a request from MediaScanner, DATA should contains file path 3199 // we only process update request from media scanner, otherwise the requests 3200 // could be duplicate. 3201 if (count > 0 && values.getAsString(MediaStore.MediaColumns.DATA) != null) { 3202 Cursor c = db.query(sGetTableAndWhereParam.table, 3203 READY_FLAG_PROJECTION, sGetTableAndWhereParam.where, 3204 whereArgs, null, null, null); 3205 if (c != null) { 3206 try { 3207 while (c.moveToNext()) { 3208 long magic = c.getLong(2); 3209 if (magic == 0) { 3210 requestMediaThumbnail(c.getString(1), uri, 3211 MediaThumbRequest.PRIORITY_NORMAL, 0); 3212 } 3213 } 3214 } finally { 3215 c.close(); 3216 } 3217 } 3218 } 3219 } 3220 break; 3221 3222 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 3223 String moveit = uri.getQueryParameter("move"); 3224 if (moveit != null) { 3225 String key = MediaStore.Audio.Playlists.Members.PLAY_ORDER; 3226 if (initialValues.containsKey(key)) { 3227 int newpos = initialValues.getAsInteger(key); 3228 List <String> segments = uri.getPathSegments(); 3229 long playlist = Long.valueOf(segments.get(3)); 3230 int oldpos = Integer.valueOf(segments.get(5)); 3231 return movePlaylistEntry(db, playlist, oldpos, newpos); 3232 } 3233 throw new IllegalArgumentException("Need to specify " + key + 3234 " when using 'move' parameter"); 3235 } 3236 // fall through 3237 default: 3238 count = db.update(sGetTableAndWhereParam.table, initialValues, 3239 sGetTableAndWhereParam.where, whereArgs); 3240 break; 3241 } 3242 } 3243 // in a transaction, the code that began the transaction should be taking 3244 // care of notifications once it ends the transaction successfully 3245 if (count > 0 && !db.inTransaction()) { 3246 getContext().getContentResolver().notifyChange(uri, null); 3247 } 3248 return count; 3249 } 3250 3251 private int movePlaylistEntry(SQLiteDatabase db, long playlist, int from, int to) { 3252 if (from == to) { 3253 return 0; 3254 } 3255 db.beginTransaction(); 3256 try { 3257 int numlines = 0; 3258 db.execSQL("UPDATE audio_playlists_map SET play_order=-1" + 3259 " WHERE play_order=" + from + 3260 " AND playlist_id=" + playlist); 3261 // We could just run both of the next two statements, but only one of 3262 // of them will actually do anything, so might as well skip the compile 3263 // and execute steps. 3264 if (from < to) { 3265 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order-1" + 3266 " WHERE play_order<=" + to + " AND play_order>" + from + 3267 " AND playlist_id=" + playlist); 3268 numlines = to - from + 1; 3269 } else { 3270 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order+1" + 3271 " WHERE play_order>=" + to + " AND play_order<" + from + 3272 " AND playlist_id=" + playlist); 3273 numlines = from - to + 1; 3274 } 3275 db.execSQL("UPDATE audio_playlists_map SET play_order=" + to + 3276 " WHERE play_order=-1 AND playlist_id=" + playlist); 3277 db.setTransactionSuccessful(); 3278 Uri uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI 3279 .buildUpon().appendEncodedPath(String.valueOf(playlist)).build(); 3280 getContext().getContentResolver().notifyChange(uri, null); 3281 return numlines; 3282 } finally { 3283 db.endTransaction(); 3284 } 3285 } 3286 3287 private static final String[] openFileColumns = new String[] { 3288 MediaStore.MediaColumns.DATA, 3289 }; 3290 3291 @Override 3292 public ParcelFileDescriptor openFile(Uri uri, String mode) 3293 throws FileNotFoundException { 3294 3295 ParcelFileDescriptor pfd = null; 3296 3297 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_FILE_ID) { 3298 // get album art for the specified media file 3299 DatabaseHelper database = getDatabaseForUri(uri); 3300 if (database == null) { 3301 throw new IllegalStateException("Couldn't open database for " + uri); 3302 } 3303 SQLiteDatabase db = database.getReadableDatabase(); 3304 if (db == null) { 3305 throw new IllegalStateException("Couldn't open database for " + uri); 3306 } 3307 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3308 int songid = Integer.parseInt(uri.getPathSegments().get(3)); 3309 qb.setTables("audio_meta"); 3310 qb.appendWhere("_id=" + songid); 3311 Cursor c = qb.query(db, 3312 new String [] { 3313 MediaStore.Audio.Media.DATA, 3314 MediaStore.Audio.Media.ALBUM_ID }, 3315 null, null, null, null, null); 3316 if (c.moveToFirst()) { 3317 String audiopath = c.getString(0); 3318 int albumid = c.getInt(1); 3319 // Try to get existing album art for this album first, which 3320 // could possibly have been obtained from a different file. 3321 // If that fails, try to get it from this specific file. 3322 Uri newUri = ContentUris.withAppendedId(ALBUMART_URI, albumid); 3323 try { 3324 pfd = openFile(newUri, mode); // recursive call 3325 } catch (FileNotFoundException ex) { 3326 // That didn't work, now try to get it from the specific file 3327 pfd = getThumb(db, audiopath, albumid, null); 3328 } 3329 } 3330 c.close(); 3331 return pfd; 3332 } 3333 3334 try { 3335 pfd = openFileHelper(uri, mode); 3336 } catch (FileNotFoundException ex) { 3337 if (mode.contains("w")) { 3338 // if the file couldn't be created, we shouldn't extract album art 3339 throw ex; 3340 } 3341 3342 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_ID) { 3343 // Tried to open an album art file which does not exist. Regenerate. 3344 DatabaseHelper database = getDatabaseForUri(uri); 3345 if (database == null) { 3346 throw ex; 3347 } 3348 SQLiteDatabase db = database.getReadableDatabase(); 3349 if (db == null) { 3350 throw new IllegalStateException("Couldn't open database for " + uri); 3351 } 3352 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3353 int albumid = Integer.parseInt(uri.getPathSegments().get(3)); 3354 qb.setTables("audio_meta"); 3355 qb.appendWhere("album_id=" + albumid); 3356 Cursor c = qb.query(db, 3357 new String [] { 3358 MediaStore.Audio.Media.DATA }, 3359 null, null, null, null, MediaStore.Audio.Media.TRACK); 3360 if (c.moveToFirst()) { 3361 String audiopath = c.getString(0); 3362 pfd = getThumb(db, audiopath, albumid, uri); 3363 } 3364 c.close(); 3365 } 3366 if (pfd == null) { 3367 throw ex; 3368 } 3369 } 3370 return pfd; 3371 } 3372 3373 private class ThumbData { 3374 SQLiteDatabase db; 3375 String path; 3376 long album_id; 3377 Uri albumart_uri; 3378 } 3379 3380 private void makeThumbAsync(SQLiteDatabase db, String path, long album_id) { 3381 synchronized (mPendingThumbs) { 3382 if (mPendingThumbs.contains(path)) { 3383 // There's already a request to make an album art thumbnail 3384 // for this audio file in the queue. 3385 return; 3386 } 3387 3388 mPendingThumbs.add(path); 3389 } 3390 3391 ThumbData d = new ThumbData(); 3392 d.db = db; 3393 d.path = path; 3394 d.album_id = album_id; 3395 d.albumart_uri = ContentUris.withAppendedId(mAlbumArtBaseUri, album_id); 3396 3397 // Instead of processing thumbnail requests in the order they were 3398 // received we instead process them stack-based, i.e. LIFO. 3399 // The idea behind this is that the most recently requested thumbnails 3400 // are most likely the ones still in the user's view, whereas those 3401 // requested earlier may have already scrolled off. 3402 synchronized (mThumbRequestStack) { 3403 mThumbRequestStack.push(d); 3404 } 3405 3406 // Trigger the handler. 3407 Message msg = mThumbHandler.obtainMessage(ALBUM_THUMB); 3408 msg.sendToTarget(); 3409 } 3410 3411 // Extract compressed image data from the audio file itself or, if that fails, 3412 // look for a file "AlbumArt.jpg" in the containing directory. 3413 private static byte[] getCompressedAlbumArt(Context context, String path) { 3414 byte[] compressed = null; 3415 3416 try { 3417 File f = new File(path); 3418 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, 3419 ParcelFileDescriptor.MODE_READ_ONLY); 3420 3421 MediaScanner scanner = new MediaScanner(context); 3422 compressed = scanner.extractAlbumArt(pfd.getFileDescriptor()); 3423 pfd.close(); 3424 3425 // If no embedded art exists, look for a suitable image file in the 3426 // same directory as the media file, except if that directory is 3427 // is the root directory of the sd card or the download directory. 3428 // We look for, in order of preference: 3429 // 0 AlbumArt.jpg 3430 // 1 AlbumArt*Large.jpg 3431 // 2 Any other jpg image with 'albumart' anywhere in the name 3432 // 3 Any other jpg image 3433 // 4 any other png image 3434 if (compressed == null && path != null) { 3435 int lastSlash = path.lastIndexOf('/'); 3436 if (lastSlash > 0) { 3437 3438 String artPath = path.substring(0, lastSlash); 3439 String sdroot = mExternalStoragePath; 3440 String dwndir = Environment.getExternalStoragePublicDirectory( 3441 Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); 3442 3443 String bestmatch = null; 3444 synchronized (sFolderArtMap) { 3445 if (sFolderArtMap.containsKey(artPath)) { 3446 bestmatch = sFolderArtMap.get(artPath); 3447 } else if (!artPath.equalsIgnoreCase(sdroot) && 3448 !artPath.equalsIgnoreCase(dwndir)) { 3449 File dir = new File(artPath); 3450 String [] entrynames = dir.list(); 3451 if (entrynames == null) { 3452 return null; 3453 } 3454 bestmatch = null; 3455 int matchlevel = 1000; 3456 for (int i = entrynames.length - 1; i >=0; i--) { 3457 String entry = entrynames[i].toLowerCase(); 3458 if (entry.equals("albumart.jpg")) { 3459 bestmatch = entrynames[i]; 3460 break; 3461 } else if (entry.startsWith("albumart") 3462 && entry.endsWith("large.jpg") 3463 && matchlevel > 1) { 3464 bestmatch = entrynames[i]; 3465 matchlevel = 1; 3466 } else if (entry.contains("albumart") 3467 && entry.endsWith(".jpg") 3468 && matchlevel > 2) { 3469 bestmatch = entrynames[i]; 3470 matchlevel = 2; 3471 } else if (entry.endsWith(".jpg") && matchlevel > 3) { 3472 bestmatch = entrynames[i]; 3473 matchlevel = 3; 3474 } else if (entry.endsWith(".png") && matchlevel > 4) { 3475 bestmatch = entrynames[i]; 3476 matchlevel = 4; 3477 } 3478 } 3479 // note that this may insert null if no album art was found 3480 sFolderArtMap.put(artPath, bestmatch); 3481 } 3482 } 3483 3484 if (bestmatch != null) { 3485 File file = new File(artPath, bestmatch); 3486 if (file.exists()) { 3487 compressed = new byte[(int)file.length()]; 3488 FileInputStream stream = null; 3489 try { 3490 stream = new FileInputStream(file); 3491 stream.read(compressed); 3492 } catch (IOException ex) { 3493 compressed = null; 3494 } finally { 3495 if (stream != null) { 3496 stream.close(); 3497 } 3498 } 3499 } 3500 } 3501 } 3502 } 3503 } catch (IOException e) { 3504 } 3505 3506 return compressed; 3507 } 3508 3509 // Return a URI to write the album art to and update the database as necessary. 3510 Uri getAlbumArtOutputUri(SQLiteDatabase db, long album_id, Uri albumart_uri) { 3511 Uri out = null; 3512 // TODO: this could be done more efficiently with a call to db.replace(), which 3513 // replaces or inserts as needed, making it unnecessary to query() first. 3514 if (albumart_uri != null) { 3515 Cursor c = query(albumart_uri, new String [] { "_data" }, 3516 null, null, null); 3517 try { 3518 if (c != null && c.moveToFirst()) { 3519 String albumart_path = c.getString(0); 3520 if (ensureFileExists(albumart_path)) { 3521 out = albumart_uri; 3522 } 3523 } else { 3524 albumart_uri = null; 3525 } 3526 } finally { 3527 if (c != null) { 3528 c.close(); 3529 } 3530 } 3531 } 3532 if (albumart_uri == null){ 3533 ContentValues initialValues = new ContentValues(); 3534 initialValues.put("album_id", album_id); 3535 try { 3536 ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 3537 long rowId = db.insert("album_art", "_data", values); 3538 if (rowId > 0) { 3539 out = ContentUris.withAppendedId(ALBUMART_URI, rowId); 3540 } 3541 } catch (IllegalStateException ex) { 3542 Log.e(TAG, "error creating album thumb file"); 3543 } 3544 } 3545 return out; 3546 } 3547 3548 // Write out the album art to the output URI, recompresses the given Bitmap 3549 // if necessary, otherwise writes the compressed data. 3550 private void writeAlbumArt( 3551 boolean need_to_recompress, Uri out, byte[] compressed, Bitmap bm) { 3552 boolean success = false; 3553 try { 3554 OutputStream outstream = getContext().getContentResolver().openOutputStream(out); 3555 3556 if (!need_to_recompress) { 3557 // No need to recompress here, just write out the original 3558 // compressed data here. 3559 outstream.write(compressed); 3560 success = true; 3561 } else { 3562 success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream); 3563 } 3564 3565 outstream.close(); 3566 } catch (FileNotFoundException ex) { 3567 Log.e(TAG, "error creating file", ex); 3568 } catch (IOException ex) { 3569 Log.e(TAG, "error creating file", ex); 3570 } 3571 if (!success) { 3572 // the thumbnail was not written successfully, delete the entry that refers to it 3573 getContext().getContentResolver().delete(out, null, null); 3574 } 3575 } 3576 3577 private ParcelFileDescriptor getThumb(SQLiteDatabase db, String path, long album_id, 3578 Uri albumart_uri) { 3579 ThumbData d = new ThumbData(); 3580 d.db = db; 3581 d.path = path; 3582 d.album_id = album_id; 3583 d.albumart_uri = albumart_uri; 3584 return makeThumbInternal(d); 3585 } 3586 3587 private ParcelFileDescriptor makeThumbInternal(ThumbData d) { 3588 byte[] compressed = getCompressedAlbumArt(getContext(), d.path); 3589 3590 if (compressed == null) { 3591 return null; 3592 } 3593 3594 Bitmap bm = null; 3595 boolean need_to_recompress = true; 3596 3597 try { 3598 // get the size of the bitmap 3599 BitmapFactory.Options opts = new BitmapFactory.Options(); 3600 opts.inJustDecodeBounds = true; 3601 opts.inSampleSize = 1; 3602 BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts); 3603 3604 // request a reasonably sized output image 3605 // TODO: don't hardcode the size 3606 while (opts.outHeight > 320 || opts.outWidth > 320) { 3607 opts.outHeight /= 2; 3608 opts.outWidth /= 2; 3609 opts.inSampleSize *= 2; 3610 } 3611 3612 if (opts.inSampleSize == 1) { 3613 // The original album art was of proper size, we won't have to 3614 // recompress the bitmap later. 3615 need_to_recompress = false; 3616 } else { 3617 // get the image for real now 3618 opts.inJustDecodeBounds = false; 3619 opts.inPreferredConfig = Bitmap.Config.RGB_565; 3620 bm = BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts); 3621 3622 if (bm != null && bm.getConfig() == null) { 3623 Bitmap nbm = bm.copy(Bitmap.Config.RGB_565, false); 3624 if (nbm != null && nbm != bm) { 3625 bm.recycle(); 3626 bm = nbm; 3627 } 3628 } 3629 } 3630 } catch (Exception e) { 3631 } 3632 3633 if (need_to_recompress && bm == null) { 3634 return null; 3635 } 3636 3637 if (d.albumart_uri == null) { 3638 // this one doesn't need to be saved (probably a song with an unknown album), 3639 // so stick it in a memory file and return that 3640 try { 3641 return ParcelFileDescriptor.fromData(compressed, "albumthumb"); 3642 } catch (IOException e) { 3643 } 3644 } else { 3645 // This one needs to actually be saved on the sd card. 3646 // This is wrapped in a transaction because there are various things 3647 // that could go wrong while generating the thumbnail, and we only want 3648 // to update the database when all steps succeeded. 3649 d.db.beginTransaction(); 3650 try { 3651 Uri out = getAlbumArtOutputUri(d.db, d.album_id, d.albumart_uri); 3652 3653 if (out != null) { 3654 writeAlbumArt(need_to_recompress, out, compressed, bm); 3655 getContext().getContentResolver().notifyChange(MEDIA_URI, null); 3656 ParcelFileDescriptor pfd = openFileHelper(out, "r"); 3657 d.db.setTransactionSuccessful(); 3658 return pfd; 3659 } 3660 } catch (FileNotFoundException ex) { 3661 // do nothing, just return null below 3662 } catch (UnsupportedOperationException ex) { 3663 // do nothing, just return null below 3664 } finally { 3665 d.db.endTransaction(); 3666 if (bm != null) { 3667 bm.recycle(); 3668 } 3669 } 3670 } 3671 return null; 3672 } 3673 3674 /** 3675 * Look up the artist or album entry for the given name, creating that entry 3676 * if it does not already exists. 3677 * @param db The database 3678 * @param table The table to store the key/name pair in. 3679 * @param keyField The name of the key-column 3680 * @param nameField The name of the name-column 3681 * @param rawName The name that the calling app was trying to insert into the database 3682 * @param cacheName The string that will be inserted in to the cache 3683 * @param path The full path to the file being inserted in to the audio table 3684 * @param albumHash A hash to distinguish between different albums of the same name 3685 * @param artist The name of the artist, if known 3686 * @param cache The cache to add this entry to 3687 * @param srcuri The Uri that prompted the call to this method, used for determining whether this is 3688 * the internal or external database 3689 * @return The row ID for this artist/album, or -1 if the provided name was invalid 3690 */ 3691 private long getKeyIdForName(SQLiteDatabase db, String table, String keyField, String nameField, 3692 String rawName, String cacheName, String path, int albumHash, 3693 String artist, HashMap<String, Long> cache, Uri srcuri) { 3694 long rowId; 3695 3696 if (rawName == null || rawName.length() == 0) { 3697 return -1; 3698 } 3699 String k = MediaStore.Audio.keyFor(rawName); 3700 3701 if (k == null) { 3702 return -1; 3703 } 3704 3705 boolean isAlbum = table.equals("albums"); 3706 boolean isUnknown = MediaStore.UNKNOWN_STRING.equals(rawName); 3707 3708 // To distinguish same-named albums, we append a hash of the path. 3709 // Ideally we would also take things like CDDB ID in to account, so 3710 // we can group files from the same album that aren't in the same 3711 // folder, but this is a quick and easy start that works immediately 3712 // without requiring support from the mp3, mp4 and Ogg meta data 3713 // readers, as long as the albums are in different folders. 3714 if (isAlbum) { 3715 k = k + albumHash; 3716 if (isUnknown) { 3717 k = k + artist; 3718 } 3719 } 3720 3721 String [] selargs = { k }; 3722 Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null); 3723 3724 try { 3725 switch (c.getCount()) { 3726 case 0: { 3727 // insert new entry into table 3728 ContentValues otherValues = new ContentValues(); 3729 otherValues.put(keyField, k); 3730 otherValues.put(nameField, rawName); 3731 rowId = db.insert(table, "duration", otherValues); 3732 if (path != null && isAlbum && ! isUnknown) { 3733 // We just inserted a new album. Now create an album art thumbnail for it. 3734 makeThumbAsync(db, path, rowId); 3735 } 3736 if (rowId > 0) { 3737 String volume = srcuri.toString().substring(16, 24); // extract internal/external 3738 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 3739 getContext().getContentResolver().notifyChange(uri, null); 3740 } 3741 } 3742 break; 3743 case 1: { 3744 // Use the existing entry 3745 c.moveToFirst(); 3746 rowId = c.getLong(0); 3747 3748 // Determine whether the current rawName is better than what's 3749 // currently stored in the table, and update the table if it is. 3750 String currentFancyName = c.getString(2); 3751 String bestName = makeBestName(rawName, currentFancyName); 3752 if (!bestName.equals(currentFancyName)) { 3753 // update the table with the new name 3754 ContentValues newValues = new ContentValues(); 3755 newValues.put(nameField, bestName); 3756 db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null); 3757 String volume = srcuri.toString().substring(16, 24); // extract internal/external 3758 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 3759 getContext().getContentResolver().notifyChange(uri, null); 3760 } 3761 } 3762 break; 3763 default: 3764 // corrupt database 3765 Log.e(TAG, "Multiple entries in table " + table + " for key " + k); 3766 rowId = -1; 3767 break; 3768 } 3769 } finally { 3770 if (c != null) c.close(); 3771 } 3772 3773 if (cache != null && ! isUnknown) { 3774 cache.put(cacheName, rowId); 3775 } 3776 return rowId; 3777 } 3778 3779 /** 3780 * Returns the best string to use for display, given two names. 3781 * Note that this function does not necessarily return either one 3782 * of the provided names; it may decide to return a better alternative 3783 * (for example, specifying the inputs "Police" and "Police, The" will 3784 * return "The Police") 3785 * 3786 * The basic assumptions are: 3787 * - longer is better ("The police" is better than "Police") 3788 * - prefix is better ("The Police" is better than "Police, The") 3789 * - accents are better ("Motörhead" is better than "Motorhead") 3790 * 3791 * @param one The first of the two names to consider 3792 * @param two The last of the two names to consider 3793 * @return The actual name to use 3794 */ 3795 String makeBestName(String one, String two) { 3796 String name; 3797 3798 // Longer names are usually better. 3799 if (one.length() > two.length()) { 3800 name = one; 3801 } else { 3802 // Names with accents are usually better, and conveniently sort later 3803 if (one.toLowerCase().compareTo(two.toLowerCase()) > 0) { 3804 name = one; 3805 } else { 3806 name = two; 3807 } 3808 } 3809 3810 // Prefixes are better than postfixes. 3811 if (name.endsWith(", the") || name.endsWith(",the") || 3812 name.endsWith(", an") || name.endsWith(",an") || 3813 name.endsWith(", a") || name.endsWith(",a")) { 3814 String fix = name.substring(1 + name.lastIndexOf(',')); 3815 name = fix.trim() + " " + name.substring(0, name.lastIndexOf(',')); 3816 } 3817 3818 // TODO: word-capitalize the resulting name 3819 return name; 3820 } 3821 3822 3823 /** 3824 * Looks up the database based on the given URI. 3825 * 3826 * @param uri The requested URI 3827 * @returns the database for the given URI 3828 */ 3829 private DatabaseHelper getDatabaseForUri(Uri uri) { 3830 synchronized (mDatabases) { 3831 if (uri.getPathSegments().size() > 1) { 3832 return mDatabases.get(uri.getPathSegments().get(0)); 3833 } 3834 } 3835 return null; 3836 } 3837 3838 /** 3839 * Attach the database for a volume (internal or external). 3840 * Does nothing if the volume is already attached, otherwise 3841 * checks the volume ID and sets up the corresponding database. 3842 * 3843 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}. 3844 * @return the content URI of the attached volume. 3845 */ 3846 private Uri attachVolume(String volume) { 3847 if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) { 3848 throw new SecurityException( 3849 "Opening and closing databases not allowed."); 3850 } 3851 3852 synchronized (mDatabases) { 3853 if (mDatabases.get(volume) != null) { // Already attached 3854 return Uri.parse("content://media/" + volume); 3855 } 3856 3857 DatabaseHelper db; 3858 if (INTERNAL_VOLUME.equals(volume)) { 3859 db = new DatabaseHelper(getContext(), INTERNAL_DATABASE_NAME, true); 3860 } else if (EXTERNAL_VOLUME.equals(volume)) { 3861 String path = mExternalStoragePath; 3862 int volumeID = FileUtils.getFatVolumeId(path); 3863 if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID); 3864 3865 // generate database name based on volume ID 3866 String dbName = "external-" + Integer.toHexString(volumeID) + ".db"; 3867 db = new DatabaseHelper(getContext(), dbName, false); 3868 mVolumeId = volumeID; 3869 } else { 3870 throw new IllegalArgumentException("There is no volume named " + volume); 3871 } 3872 3873 mDatabases.put(volume, db); 3874 3875 if (!db.mInternal) { 3876 // create default directories (only happens on first boot) 3877 createDefaultFolders(db.getWritableDatabase()); 3878 3879 // clean up stray album art files: delete every file not in the database 3880 File[] files = new File(mExternalStoragePath, ALBUM_THUMB_FOLDER).listFiles(); 3881 HashSet<String> fileSet = new HashSet(); 3882 for (int i = 0; files != null && i < files.length; i++) { 3883 fileSet.add(files[i].getPath()); 3884 } 3885 3886 Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, 3887 new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null); 3888 try { 3889 while (cursor != null && cursor.moveToNext()) { 3890 fileSet.remove(cursor.getString(0)); 3891 } 3892 } finally { 3893 if (cursor != null) cursor.close(); 3894 } 3895 3896 Iterator<String> iterator = fileSet.iterator(); 3897 while (iterator.hasNext()) { 3898 String filename = iterator.next(); 3899 if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename); 3900 new File(filename).delete(); 3901 } 3902 } 3903 } 3904 3905 if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume); 3906 return Uri.parse("content://media/" + volume); 3907 } 3908 3909 /** 3910 * Detach the database for a volume (must be external). 3911 * Does nothing if the volume is already detached, otherwise 3912 * closes the database and sends a notification to listeners. 3913 * 3914 * @param uri The content URI of the volume, as returned by {@link #attachVolume} 3915 */ 3916 private void detachVolume(Uri uri) { 3917 if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) { 3918 throw new SecurityException( 3919 "Opening and closing databases not allowed."); 3920 } 3921 3922 String volume = uri.getPathSegments().get(0); 3923 if (INTERNAL_VOLUME.equals(volume)) { 3924 throw new UnsupportedOperationException( 3925 "Deleting the internal volume is not allowed"); 3926 } else if (!EXTERNAL_VOLUME.equals(volume)) { 3927 throw new IllegalArgumentException( 3928 "There is no volume named " + volume); 3929 } 3930 3931 synchronized (mDatabases) { 3932 DatabaseHelper database = mDatabases.get(volume); 3933 if (database == null) return; 3934 3935 try { 3936 // touch the database file to show it is most recently used 3937 File file = new File(database.getReadableDatabase().getPath()); 3938 file.setLastModified(System.currentTimeMillis()); 3939 } catch (SQLException e) { 3940 Log.e(TAG, "Can't touch database file", e); 3941 } 3942 3943 mDatabases.remove(volume); 3944 database.close(); 3945 } 3946 3947 getContext().getContentResolver().notifyChange(uri, null); 3948 if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume); 3949 } 3950 3951 private static String TAG = "MediaProvider"; 3952 private static final boolean LOCAL_LOGV = false; 3953 private static final int DATABASE_VERSION = 302; 3954 private static final String INTERNAL_DATABASE_NAME = "internal.db"; 3955 3956 // maximum number of cached external databases to keep 3957 private static final int MAX_EXTERNAL_DATABASES = 3; 3958 3959 // Delete databases that have not been used in two months 3960 // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60) 3961 private static final long OBSOLETE_DATABASE_DB = 5184000000L; 3962 3963 private HashMap<String, DatabaseHelper> mDatabases; 3964 3965 private Handler mThumbHandler; 3966 3967 // name of the volume currently being scanned by the media scanner (or null) 3968 private String mMediaScannerVolume; 3969 3970 // current FAT volume ID 3971 private int mVolumeId; 3972 3973 static final String INTERNAL_VOLUME = "internal"; 3974 static final String EXTERNAL_VOLUME = "external"; 3975 static final String ALBUM_THUMB_FOLDER = "Android/data/com.android.providers.media/albumthumbs"; 3976 3977 // path for writing contents of in memory temp database 3978 private String mTempDatabasePath; 3979 3980 // WARNING: the values of IMAGES_MEDIA, AUDIO_MEDIA, and VIDEO_MEDIA and AUDIO_PLAYLISTS 3981 // are stored in the "files" table, so do not renumber them unless you also add 3982 // a corresponding database upgrade step for it. 3983 private static final int IMAGES_MEDIA = 1; 3984 private static final int IMAGES_MEDIA_ID = 2; 3985 private static final int IMAGES_THUMBNAILS = 3; 3986 private static final int IMAGES_THUMBNAILS_ID = 4; 3987 3988 private static final int AUDIO_MEDIA = 100; 3989 private static final int AUDIO_MEDIA_ID = 101; 3990 private static final int AUDIO_MEDIA_ID_GENRES = 102; 3991 private static final int AUDIO_MEDIA_ID_GENRES_ID = 103; 3992 private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104; 3993 private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105; 3994 private static final int AUDIO_GENRES = 106; 3995 private static final int AUDIO_GENRES_ID = 107; 3996 private static final int AUDIO_GENRES_ID_MEMBERS = 108; 3997 private static final int AUDIO_GENRES_ID_MEMBERS_ID = 109; 3998 private static final int AUDIO_PLAYLISTS = 110; 3999 private static final int AUDIO_PLAYLISTS_ID = 111; 4000 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 4001 private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113; 4002 private static final int AUDIO_ARTISTS = 114; 4003 private static final int AUDIO_ARTISTS_ID = 115; 4004 private static final int AUDIO_ALBUMS = 116; 4005 private static final int AUDIO_ALBUMS_ID = 117; 4006 private static final int AUDIO_ARTISTS_ID_ALBUMS = 118; 4007 private static final int AUDIO_ALBUMART = 119; 4008 private static final int AUDIO_ALBUMART_ID = 120; 4009 private static final int AUDIO_ALBUMART_FILE_ID = 121; 4010 4011 private static final int VIDEO_MEDIA = 200; 4012 private static final int VIDEO_MEDIA_ID = 201; 4013 private static final int VIDEO_THUMBNAILS = 202; 4014 private static final int VIDEO_THUMBNAILS_ID = 203; 4015 4016 private static final int VOLUMES = 300; 4017 private static final int VOLUMES_ID = 301; 4018 4019 private static final int AUDIO_SEARCH_LEGACY = 400; 4020 private static final int AUDIO_SEARCH_BASIC = 401; 4021 private static final int AUDIO_SEARCH_FANCY = 402; 4022 4023 private static final int MEDIA_SCANNER = 500; 4024 4025 private static final int FS_ID = 600; 4026 4027 private static final int FILES = 700; 4028 private static final int FILES_ID = 701; 4029 4030 // Used only by the MTP implementation 4031 private static final int MTP_OBJECTS = 702; 4032 private static final int MTP_OBJECTS_ID = 703; 4033 private static final int MTP_OBJECT_REFERENCES = 704; 4034 4035 private static final UriMatcher URI_MATCHER = 4036 new UriMatcher(UriMatcher.NO_MATCH); 4037 4038 private static final String[] ID_PROJECTION = new String[] { 4039 MediaStore.MediaColumns._ID 4040 }; 4041 4042 private static final String[] PATH_PROJECTION = new String[] { 4043 MediaStore.MediaColumns._ID, 4044 MediaStore.MediaColumns.DATA, 4045 }; 4046 4047 private static final String[] MIME_TYPE_PROJECTION = new String[] { 4048 MediaStore.MediaColumns._ID, // 0 4049 MediaStore.MediaColumns.MIME_TYPE, // 1 4050 }; 4051 4052 private static final String[] READY_FLAG_PROJECTION = new String[] { 4053 MediaStore.MediaColumns._ID, 4054 MediaStore.MediaColumns.DATA, 4055 Images.Media.MINI_THUMB_MAGIC 4056 }; 4057 4058 private static final String OBJECT_REFERENCES_QUERY = 4059 "SELECT " + Audio.Playlists.Members.AUDIO_ID + " FROM audio_playlists_map" 4060 + " WHERE " + Audio.Playlists.Members.PLAYLIST_ID + "=?" 4061 + " ORDER BY " + Audio.Playlists.Members.PLAY_ORDER; 4062 4063 static 4064 { 4065 URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA); 4066 URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID); 4067 URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS); 4068 URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID); 4069 4070 URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA); 4071 URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID); 4072 URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES); 4073 URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID); 4074 URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS); 4075 URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID); 4076 URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES); 4077 URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID); 4078 URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS); 4079 URI_MATCHER.addURI("media", "*/audio/genres/#/members/#", AUDIO_GENRES_ID_MEMBERS_ID); 4080 URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS); 4081 URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID); 4082 URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS); 4083 URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID); 4084 URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS); 4085 URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID); 4086 URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS); 4087 URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS); 4088 URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID); 4089 URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART); 4090 URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID); 4091 URI_MATCHER.addURI("media", "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID); 4092 4093 URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA); 4094 URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID); 4095 URI_MATCHER.addURI("media", "*/video/thumbnails", VIDEO_THUMBNAILS); 4096 URI_MATCHER.addURI("media", "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID); 4097 4098 URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER); 4099 4100 URI_MATCHER.addURI("media", "*/fs_id", FS_ID); 4101 4102 URI_MATCHER.addURI("media", "*", VOLUMES_ID); 4103 URI_MATCHER.addURI("media", null, VOLUMES); 4104 4105 // Used by MTP implementation 4106 URI_MATCHER.addURI("media", "*/file", FILES); 4107 URI_MATCHER.addURI("media", "*/file/#", FILES_ID); 4108 URI_MATCHER.addURI("media", "*/object", MTP_OBJECTS); 4109 URI_MATCHER.addURI("media", "*/object/#", MTP_OBJECTS_ID); 4110 URI_MATCHER.addURI("media", "*/object/#/references", MTP_OBJECT_REFERENCES); 4111 4112 /** 4113 * @deprecated use the 'basic' or 'fancy' search Uris instead 4114 */ 4115 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY, 4116 AUDIO_SEARCH_LEGACY); 4117 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 4118 AUDIO_SEARCH_LEGACY); 4119 4120 // used for search suggestions 4121 URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY, 4122 AUDIO_SEARCH_BASIC); 4123 URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY + 4124 "/*", AUDIO_SEARCH_BASIC); 4125 4126 // used by the music app's search activity 4127 URI_MATCHER.addURI("media", "*/audio/search/fancy", AUDIO_SEARCH_FANCY); 4128 URI_MATCHER.addURI("media", "*/audio/search/fancy/*", AUDIO_SEARCH_FANCY); 4129 } 4130} 4131