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