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