1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media; 18 19 import static android.Manifest.permission.ACCESS_CACHE_FILESYSTEM; 20 import static android.Manifest.permission.READ_EXTERNAL_STORAGE; 21 import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; 22 import static android.Manifest.permission.WRITE_MEDIA_STORAGE; 23 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; 24 import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY; 25 26 import android.app.AppOpsManager; 27 import android.app.SearchManager; 28 import android.content.BroadcastReceiver; 29 import android.content.ComponentName; 30 import android.content.ContentProvider; 31 import android.content.ContentProviderOperation; 32 import android.content.ContentProviderResult; 33 import android.content.ContentResolver; 34 import android.content.ContentUris; 35 import android.content.ContentValues; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.IntentFilter; 39 import android.content.OperationApplicationException; 40 import android.content.ServiceConnection; 41 import android.content.SharedPreferences; 42 import android.content.UriMatcher; 43 import android.content.pm.PackageManager; 44 import android.content.pm.PackageManager.NameNotFoundException; 45 import android.content.res.Resources; 46 import android.database.Cursor; 47 import android.database.DatabaseUtils; 48 import android.database.MatrixCursor; 49 import android.database.sqlite.SQLiteDatabase; 50 import android.database.sqlite.SQLiteOpenHelper; 51 import android.database.sqlite.SQLiteQueryBuilder; 52 import android.graphics.Bitmap; 53 import android.graphics.BitmapFactory; 54 import android.media.MediaFile; 55 import android.media.MediaScanner; 56 import android.media.MediaScannerConnection; 57 import android.media.MediaScannerConnection.MediaScannerConnectionClient; 58 import android.media.MiniThumbFile; 59 import android.mtp.MtpConstants; 60 import android.net.Uri; 61 import android.os.Binder; 62 import android.os.Bundle; 63 import android.os.Environment; 64 import android.os.Handler; 65 import android.os.HandlerThread; 66 import android.os.Message; 67 import android.os.ParcelFileDescriptor; 68 import android.os.Process; 69 import android.os.RemoteException; 70 import android.os.SystemClock; 71 import android.os.storage.StorageManager; 72 import android.os.storage.StorageVolume; 73 import android.os.storage.VolumeInfo; 74 import android.preference.PreferenceManager; 75 import android.provider.BaseColumns; 76 import android.provider.MediaStore; 77 import android.provider.MediaStore.Audio; 78 import android.provider.MediaStore.Audio.Playlists; 79 import android.provider.MediaStore.Files; 80 import android.provider.MediaStore.Files.FileColumns; 81 import android.provider.MediaStore.Images; 82 import android.provider.MediaStore.Images.ImageColumns; 83 import android.provider.MediaStore.MediaColumns; 84 import android.provider.MediaStore.Video; 85 import android.system.ErrnoException; 86 import android.system.Os; 87 import android.system.OsConstants; 88 import android.system.StructStat; 89 import android.text.TextUtils; 90 import android.text.format.DateUtils; 91 import android.util.Log; 92 93 import libcore.io.IoUtils; 94 import libcore.util.EmptyArray; 95 96 import java.io.File; 97 import java.io.FileDescriptor; 98 import java.io.FileInputStream; 99 import java.io.FileNotFoundException; 100 import java.io.IOException; 101 import java.io.OutputStream; 102 import java.io.PrintWriter; 103 import java.util.ArrayList; 104 import java.util.Collection; 105 import java.util.HashMap; 106 import java.util.HashSet; 107 import java.util.Iterator; 108 import java.util.List; 109 import java.util.Locale; 110 import java.util.PriorityQueue; 111 import java.util.Stack; 112 113 /** 114 * Media content provider. See {@link android.provider.MediaStore} for details. 115 * Separate databases are kept for each external storage card we see (using the 116 * card's ID as an index). The content visible at content://media/external/... 117 * changes with the card. 118 */ 119 public class MediaProvider extends ContentProvider { 120 private static final Uri MEDIA_URI = Uri.parse("content://media"); 121 private static final Uri ALBUMART_URI = Uri.parse("content://media/external/audio/albumart"); 122 private static final int ALBUM_THUMB = 1; 123 private static final int IMAGE_THUMB = 2; 124 125 private static final HashMap<String, String> sArtistAlbumsMap = new HashMap<String, String>(); 126 private static final HashMap<String, String> sFolderArtMap = new HashMap<String, String>(); 127 128 /** Resolved canonical path to external storage. */ 129 private String mExternalPath; 130 /** Resolved canonical path to cache storage. */ 131 private String mCachePath; 132 /** Resolved canonical path to legacy storage. */ 133 private String mLegacyPath; 134 updateStoragePaths()135 private void updateStoragePaths() { 136 mExternalStoragePaths = mStorageManager.getVolumePaths(); 137 138 try { 139 mExternalPath = 140 Environment.getExternalStorageDirectory().getCanonicalPath() + File.separator; 141 mCachePath = 142 Environment.getDownloadCacheDirectory().getCanonicalPath() + File.separator; 143 mLegacyPath = 144 Environment.getLegacyExternalStorageDirectory().getCanonicalPath() 145 + File.separator; 146 } catch (IOException e) { 147 throw new RuntimeException("Unable to resolve canonical paths", e); 148 } 149 } 150 151 private StorageManager mStorageManager; 152 private AppOpsManager mAppOpsManager; 153 154 // In memory cache of path<->id mappings, to speed up inserts during media scan 155 HashMap<String, Long> mDirectoryCache = new HashMap<String, Long>(); 156 157 // A HashSet of paths that are pending creation of album art thumbnails. 158 private HashSet mPendingThumbs = new HashSet(); 159 160 // A Stack of outstanding thumbnail requests. 161 private Stack mThumbRequestStack = new Stack(); 162 163 // The lock of mMediaThumbQueue protects both mMediaThumbQueue and mCurrentThumbRequest. 164 private MediaThumbRequest mCurrentThumbRequest = null; 165 private PriorityQueue<MediaThumbRequest> mMediaThumbQueue = 166 new PriorityQueue<MediaThumbRequest>(MediaThumbRequest.PRIORITY_NORMAL, 167 MediaThumbRequest.getComparator()); 168 169 private boolean mCaseInsensitivePaths; 170 private String[] mExternalStoragePaths = EmptyArray.STRING; 171 172 // For compatibility with the approximately 0 apps that used mediaprovider search in 173 // releases 1.0, 1.1 or 1.5 174 private String[] mSearchColsLegacy = new String[] { 175 android.provider.BaseColumns._ID, 176 MediaStore.Audio.Media.MIME_TYPE, 177 "(CASE WHEN grouporder=1 THEN " + R.drawable.ic_search_category_music_artist + 178 " ELSE CASE WHEN grouporder=2 THEN " + R.drawable.ic_search_category_music_album + 179 " ELSE " + R.drawable.ic_search_category_music_song + " END END" + 180 ") AS " + SearchManager.SUGGEST_COLUMN_ICON_1, 181 "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2, 182 "text1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 183 "text1 AS " + SearchManager.SUGGEST_COLUMN_QUERY, 184 "CASE when grouporder=1 THEN data1 ELSE artist END AS data1", 185 "CASE when grouporder=1 THEN data2 ELSE " + 186 "CASE WHEN grouporder=2 THEN NULL ELSE album END END AS data2", 187 "match as ar", 188 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 189 "grouporder", 190 "NULL AS itemorder" // We should be sorting by the artist/album/title keys, but that 191 // column is not available here, and the list is already sorted. 192 }; 193 private String[] mSearchColsFancy = new String[] { 194 android.provider.BaseColumns._ID, 195 MediaStore.Audio.Media.MIME_TYPE, 196 MediaStore.Audio.Artists.ARTIST, 197 MediaStore.Audio.Albums.ALBUM, 198 MediaStore.Audio.Media.TITLE, 199 "data1", 200 "data2", 201 }; 202 // If this array gets changed, please update the constant below to point to the correct item. 203 private String[] mSearchColsBasic = new String[] { 204 android.provider.BaseColumns._ID, 205 MediaStore.Audio.Media.MIME_TYPE, 206 "(CASE WHEN grouporder=1 THEN " + R.drawable.ic_search_category_music_artist + 207 " ELSE CASE WHEN grouporder=2 THEN " + R.drawable.ic_search_category_music_album + 208 " ELSE " + R.drawable.ic_search_category_music_song + " END END" + 209 ") AS " + SearchManager.SUGGEST_COLUMN_ICON_1, 210 "text1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 211 "text1 AS " + SearchManager.SUGGEST_COLUMN_QUERY, 212 "(CASE WHEN grouporder=1 THEN '%1'" + // %1 gets replaced with localized string. 213 " ELSE CASE WHEN grouporder=3 THEN artist || ' - ' || album" + 214 " ELSE CASE WHEN text2!='" + MediaStore.UNKNOWN_STRING + "' THEN text2" + 215 " ELSE NULL END END END) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2, 216 SearchManager.SUGGEST_COLUMN_INTENT_DATA 217 }; 218 // Position of the TEXT_2 item in the above array. 219 private final int SEARCH_COLUMN_BASIC_TEXT2 = 5; 220 221 private static final String[] sMediaTableColumns = new String[] { 222 FileColumns._ID, 223 FileColumns.MEDIA_TYPE, 224 }; 225 226 private static final String[] sIdOnlyColumn = new String[] { 227 FileColumns._ID 228 }; 229 230 private static final String[] sDataOnlyColumn = new String[] { 231 FileColumns.DATA 232 }; 233 234 private static final String[] sMediaTypeDataId = new String[] { 235 FileColumns.MEDIA_TYPE, 236 FileColumns.DATA, 237 FileColumns._ID 238 }; 239 240 private static final String[] sPlaylistIdPlayOrder = new String[] { 241 Playlists.Members.PLAYLIST_ID, 242 Playlists.Members.PLAY_ORDER 243 }; 244 245 private Uri mAlbumArtBaseUri = Uri.parse("content://media/external/audio/albumart"); 246 247 private static final String CANONICAL = "canonical"; 248 249 private BroadcastReceiver mUnmountReceiver = new BroadcastReceiver() { 250 @Override 251 public void onReceive(Context context, Intent intent) { 252 if (Intent.ACTION_MEDIA_EJECT.equals(intent.getAction())) { 253 StorageVolume storage = (StorageVolume)intent.getParcelableExtra( 254 StorageVolume.EXTRA_STORAGE_VOLUME); 255 // If primary external storage is ejected, then remove the external volume 256 // notify all cursors backed by data on that volume. 257 if (storage.getPath().equals(mExternalStoragePaths[0])) { 258 detachVolume(Uri.parse("content://media/external")); 259 sFolderArtMap.clear(); 260 MiniThumbFile.reset(); 261 } else { 262 // If secondary external storage is ejected, then we delete all database 263 // entries for that storage from the files table. 264 DatabaseHelper database; 265 synchronized (mDatabases) { 266 // This synchronized block is limited to avoid a potential deadlock 267 // with bulkInsert() method. 268 database = mDatabases.get(EXTERNAL_VOLUME); 269 } 270 Uri uri = Uri.parse("file://" + storage.getPath()); 271 if (database != null) { 272 try { 273 // Send media scanner started and stopped broadcasts for apps that rely 274 // on these Intents for coarse grained media database notifications. 275 context.sendBroadcast( 276 new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri)); 277 278 // don't send objectRemoved events - MTP be sending StorageRemoved anyway 279 mDisableMtpObjectCallbacks = true; 280 Log.d(TAG, "deleting all entries for storage " + storage); 281 SQLiteDatabase db = database.getWritableDatabase(); 282 // First clear the file path to disable the _DELETE_FILE database hook. 283 // We do this to avoid deleting files if the volume is remounted while 284 // we are still processing the unmount event. 285 ContentValues values = new ContentValues(); 286 values.putNull(Files.FileColumns.DATA); 287 String where = FileColumns.STORAGE_ID + "=?"; 288 String[] whereArgs = new String[] { Integer.toString(storage.getStorageId()) }; 289 database.mNumUpdates++; 290 db.beginTransaction(); 291 try { 292 db.update("files", values, where, whereArgs); 293 // now delete the records 294 database.mNumDeletes++; 295 int numpurged = db.delete("files", where, whereArgs); 296 logToDb(db, "removed " + numpurged + 297 " rows for ejected filesystem " + storage.getPath()); 298 db.setTransactionSuccessful(); 299 } finally { 300 db.endTransaction(); 301 } 302 // notify on media Uris as well as the files Uri 303 context.getContentResolver().notifyChange( 304 Audio.Media.getContentUri(EXTERNAL_VOLUME), null); 305 context.getContentResolver().notifyChange( 306 Images.Media.getContentUri(EXTERNAL_VOLUME), null); 307 context.getContentResolver().notifyChange( 308 Video.Media.getContentUri(EXTERNAL_VOLUME), null); 309 context.getContentResolver().notifyChange( 310 Files.getContentUri(EXTERNAL_VOLUME), null); 311 } catch (Exception e) { 312 Log.e(TAG, "exception deleting storage entries", e); 313 } finally { 314 context.sendBroadcast( 315 new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri)); 316 mDisableMtpObjectCallbacks = false; 317 } 318 } 319 } 320 } 321 } 322 }; 323 324 // set to disable sending events when the operation originates from MTP 325 private boolean mDisableMtpObjectCallbacks; 326 327 private final SQLiteDatabase.CustomFunction mObjectRemovedCallback = 328 new SQLiteDatabase.CustomFunction() { 329 public void callback(String[] args) { 330 // We could remove only the deleted entry from the cache, but that 331 // requires the path, which we don't have here, so instead we just 332 // clear the entire cache. 333 // TODO: include the path in the callback and only remove the affected 334 // entry from the cache 335 mDirectoryCache.clear(); 336 // do nothing if the operation originated from MTP 337 if (mDisableMtpObjectCallbacks) return; 338 339 Log.d(TAG, "object removed " + args[0]); 340 IMtpService mtpService = mMtpService; 341 if (mtpService != null) { 342 try { 343 sendObjectRemoved(Integer.parseInt(args[0])); 344 } catch (NumberFormatException e) { 345 Log.e(TAG, "NumberFormatException in mObjectRemovedCallback", e); 346 } 347 } 348 } 349 }; 350 351 /** 352 * Wrapper class for a specific database (associated with one particular 353 * external card, or with internal storage). Can open the actual database 354 * on demand, create and upgrade the schema, etc. 355 */ 356 static final class DatabaseHelper extends SQLiteOpenHelper { 357 final Context mContext; 358 final String mName; 359 final boolean mInternal; // True if this is the internal database 360 final boolean mEarlyUpgrade; 361 final SQLiteDatabase.CustomFunction mObjectRemovedCallback; 362 boolean mUpgradeAttempted; // Used for upgrade error handling 363 int mNumQueries; 364 int mNumUpdates; 365 int mNumInserts; 366 int mNumDeletes; 367 long mScanStartTime; 368 long mScanStopTime; 369 370 // In memory caches of artist and album data. 371 HashMap<String, Long> mArtistCache = new HashMap<String, Long>(); 372 HashMap<String, Long> mAlbumCache = new HashMap<String, Long>(); 373 DatabaseHelper(Context context, String name, boolean internal, boolean earlyUpgrade, SQLiteDatabase.CustomFunction objectRemovedCallback)374 public DatabaseHelper(Context context, String name, boolean internal, 375 boolean earlyUpgrade, 376 SQLiteDatabase.CustomFunction objectRemovedCallback) { 377 super(context, name, null, getDatabaseVersion(context)); 378 mContext = context; 379 mName = name; 380 mInternal = internal; 381 mEarlyUpgrade = earlyUpgrade; 382 mObjectRemovedCallback = objectRemovedCallback; 383 setWriteAheadLoggingEnabled(true); 384 } 385 386 /** 387 * Creates database the first time we try to open it. 388 */ 389 @Override onCreate(final SQLiteDatabase db)390 public void onCreate(final SQLiteDatabase db) { 391 updateDatabase(mContext, db, mInternal, 0, getDatabaseVersion(mContext)); 392 } 393 394 /** 395 * Updates the database format when a new content provider is used 396 * with an older database format. 397 */ 398 @Override onUpgrade(final SQLiteDatabase db, final int oldV, final int newV)399 public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) { 400 mUpgradeAttempted = true; 401 updateDatabase(mContext, db, mInternal, oldV, newV); 402 } 403 404 @Override getWritableDatabase()405 public synchronized SQLiteDatabase getWritableDatabase() { 406 SQLiteDatabase result = null; 407 mUpgradeAttempted = false; 408 try { 409 result = super.getWritableDatabase(); 410 } catch (Exception e) { 411 if (!mUpgradeAttempted) { 412 Log.e(TAG, "failed to open database " + mName, e); 413 return null; 414 } 415 } 416 417 // If we failed to open the database during an upgrade, delete the file and try again. 418 // This will result in the creation of a fresh database, which will be repopulated 419 // when the media scanner runs. 420 if (result == null && mUpgradeAttempted) { 421 mContext.deleteDatabase(mName); 422 result = super.getWritableDatabase(); 423 } 424 return result; 425 } 426 427 /** 428 * For devices that have removable storage, we support keeping multiple databases 429 * to allow users to switch between a number of cards. 430 * On such devices, touch this particular database and garbage collect old databases. 431 * An LRU cache system is used to clean up databases for old external 432 * storage volumes. 433 */ 434 @Override onOpen(SQLiteDatabase db)435 public void onOpen(SQLiteDatabase db) { 436 437 if (mInternal) return; // The internal database is kept separately. 438 439 if (mEarlyUpgrade) return; // Doing early upgrade. 440 441 if (mObjectRemovedCallback != null) { 442 db.addCustomFunction("_OBJECT_REMOVED", 1, mObjectRemovedCallback); 443 } 444 445 // the code below is only needed on devices with removable storage 446 if (!Environment.isExternalStorageRemovable()) return; 447 448 // touch the database file to show it is most recently used 449 File file = new File(db.getPath()); 450 long now = System.currentTimeMillis(); 451 file.setLastModified(now); 452 453 // delete least recently used databases if we are over the limit 454 String[] databases = mContext.databaseList(); 455 // Don't delete wal auxiliary files(db-shm and db-wal) directly because db file may 456 // not be deleted, and it will cause Disk I/O error when accessing this database. 457 List<String> dbList = new ArrayList<String>(); 458 for (String database : databases) { 459 if (database != null && database.endsWith(".db")) { 460 dbList.add(database); 461 } 462 } 463 databases = dbList.toArray(new String[0]); 464 int count = databases.length; 465 int limit = MAX_EXTERNAL_DATABASES; 466 467 // delete external databases that have not been used in the past two months 468 long twoMonthsAgo = now - OBSOLETE_DATABASE_DB; 469 for (int i = 0; i < databases.length; i++) { 470 File other = mContext.getDatabasePath(databases[i]); 471 if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) { 472 databases[i] = null; 473 count--; 474 if (file.equals(other)) { 475 // reduce limit to account for the existence of the database we 476 // are about to open, which we removed from the list. 477 limit--; 478 } 479 } else { 480 long time = other.lastModified(); 481 if (time < twoMonthsAgo) { 482 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[i]); 483 mContext.deleteDatabase(databases[i]); 484 databases[i] = null; 485 count--; 486 } 487 } 488 } 489 490 // delete least recently used databases until 491 // we are no longer over the limit 492 while (count > limit) { 493 int lruIndex = -1; 494 long lruTime = 0; 495 496 for (int i = 0; i < databases.length; i++) { 497 if (databases[i] != null) { 498 long time = mContext.getDatabasePath(databases[i]).lastModified(); 499 if (lruTime == 0 || time < lruTime) { 500 lruIndex = i; 501 lruTime = time; 502 } 503 } 504 } 505 506 // delete least recently used database 507 if (lruIndex != -1) { 508 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]); 509 mContext.deleteDatabase(databases[lruIndex]); 510 databases[lruIndex] = null; 511 count--; 512 } 513 } 514 } 515 } 516 517 // synchronize on mMtpServiceConnection when accessing mMtpService 518 private IMtpService mMtpService; 519 520 private final ServiceConnection mMtpServiceConnection = new ServiceConnection() { 521 public void onServiceConnected(ComponentName className, android.os.IBinder service) { 522 synchronized (this) { 523 mMtpService = IMtpService.Stub.asInterface(service); 524 } 525 } 526 527 public void onServiceDisconnected(ComponentName className) { 528 synchronized (this) { 529 mMtpService = null; 530 } 531 } 532 }; 533 534 private static final String[] sDefaultFolderNames = { 535 Environment.DIRECTORY_MUSIC, 536 Environment.DIRECTORY_PODCASTS, 537 Environment.DIRECTORY_RINGTONES, 538 Environment.DIRECTORY_ALARMS, 539 Environment.DIRECTORY_NOTIFICATIONS, 540 Environment.DIRECTORY_PICTURES, 541 Environment.DIRECTORY_MOVIES, 542 Environment.DIRECTORY_DOWNLOADS, 543 Environment.DIRECTORY_DCIM, 544 }; 545 546 /** 547 * Ensure that default folders are created on mounted primary storage 548 * devices. We only do this once per volume so we don't annoy the user if 549 * deleted manually. 550 */ ensureDefaultFolders(DatabaseHelper helper, SQLiteDatabase db)551 private void ensureDefaultFolders(DatabaseHelper helper, SQLiteDatabase db) { 552 final StorageVolume vol = mStorageManager.getPrimaryVolume(); 553 final String key; 554 if (VolumeInfo.ID_EMULATED_INTERNAL.equals(vol.getId())) { 555 key = "created_default_folders"; 556 } else { 557 key = "created_default_folders_" + vol.getUuid(); 558 } 559 560 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 561 if (prefs.getInt(key, 0) == 0) { 562 for (String folderName : sDefaultFolderNames) { 563 final File folder = new File(vol.getPathFile(), folderName); 564 if (!folder.exists()) { 565 folder.mkdirs(); 566 insertDirectory(helper, db, folder.getAbsolutePath()); 567 } 568 } 569 570 SharedPreferences.Editor editor = prefs.edit(); 571 editor.putInt(key, 1); 572 editor.commit(); 573 } 574 } 575 getDatabaseVersion(Context context)576 public static int getDatabaseVersion(Context context) { 577 try { 578 return context.getPackageManager().getPackageInfo( 579 context.getPackageName(), 0).versionCode; 580 } catch (NameNotFoundException e) { 581 throw new RuntimeException("couldn't get version code for " + context); 582 } 583 } 584 585 @Override onCreate()586 public boolean onCreate() { 587 final Context context = getContext(); 588 589 mStorageManager = context.getSystemService(StorageManager.class); 590 mAppOpsManager = context.getSystemService(AppOpsManager.class); 591 592 sArtistAlbumsMap.put(MediaStore.Audio.Albums._ID, "audio.album_id AS " + 593 MediaStore.Audio.Albums._ID); 594 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM, "album"); 595 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_KEY, "album_key"); 596 sArtistAlbumsMap.put(MediaStore.Audio.Albums.FIRST_YEAR, "MIN(year) AS " + 597 MediaStore.Audio.Albums.FIRST_YEAR); 598 sArtistAlbumsMap.put(MediaStore.Audio.Albums.LAST_YEAR, "MAX(year) AS " + 599 MediaStore.Audio.Albums.LAST_YEAR); 600 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST, "artist"); 601 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_ID, "artist"); 602 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_KEY, "artist_key"); 603 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS, "count(*) AS " + 604 MediaStore.Audio.Albums.NUMBER_OF_SONGS); 605 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_ART, "album_art._data AS " + 606 MediaStore.Audio.Albums.ALBUM_ART); 607 608 mSearchColsBasic[SEARCH_COLUMN_BASIC_TEXT2] = 609 mSearchColsBasic[SEARCH_COLUMN_BASIC_TEXT2].replaceAll( 610 "%1", context.getString(R.string.artist_label)); 611 mDatabases = new HashMap<String, DatabaseHelper>(); 612 attachVolume(INTERNAL_VOLUME); 613 614 IntentFilter iFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); 615 iFilter.addDataScheme("file"); 616 context.registerReceiver(mUnmountReceiver, iFilter); 617 618 // open external database if external storage is mounted 619 String state = Environment.getExternalStorageState(); 620 if (Environment.MEDIA_MOUNTED.equals(state) || 621 Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 622 attachVolume(EXTERNAL_VOLUME); 623 } 624 625 HandlerThread ht = new HandlerThread("thumbs thread", Process.THREAD_PRIORITY_BACKGROUND); 626 ht.start(); 627 mThumbHandler = new Handler(ht.getLooper()) { 628 @Override 629 public void handleMessage(Message msg) { 630 if (msg.what == IMAGE_THUMB) { 631 synchronized (mMediaThumbQueue) { 632 mCurrentThumbRequest = mMediaThumbQueue.poll(); 633 } 634 if (mCurrentThumbRequest == null) { 635 Log.w(TAG, "Have message but no request?"); 636 } else { 637 try { 638 if (mCurrentThumbRequest.mPath != null) { 639 File origFile = new File(mCurrentThumbRequest.mPath); 640 if (origFile.exists() && origFile.length() > 0) { 641 mCurrentThumbRequest.execute(); 642 // Check if more requests for the same image are queued. 643 synchronized (mMediaThumbQueue) { 644 for (MediaThumbRequest mtq : mMediaThumbQueue) { 645 if ((mtq.mOrigId == mCurrentThumbRequest.mOrigId) && 646 (mtq.mIsVideo == mCurrentThumbRequest.mIsVideo) && 647 (mtq.mMagic == 0) && 648 (mtq.mState == MediaThumbRequest.State.WAIT)) { 649 mtq.mMagic = mCurrentThumbRequest.mMagic; 650 } 651 } 652 } 653 } else { 654 // original file hasn't been stored yet 655 synchronized (mMediaThumbQueue) { 656 Log.w(TAG, "original file hasn't been stored yet: " + mCurrentThumbRequest.mPath); 657 } 658 } 659 } 660 } catch (IOException ex) { 661 Log.w(TAG, ex); 662 } catch (UnsupportedOperationException ex) { 663 // This could happen if we unplug the sd card during insert/update/delete 664 // See getDatabaseForUri. 665 Log.w(TAG, ex); 666 } catch (OutOfMemoryError err) { 667 /* 668 * Note: Catching Errors is in most cases considered 669 * bad practice. However, in this case it is 670 * motivated by the fact that corrupt or very large 671 * images may cause a huge allocation to be 672 * requested and denied. The bitmap handling API in 673 * Android offers no other way to guard against 674 * these problems than by catching OutOfMemoryError. 675 */ 676 Log.w(TAG, err); 677 } finally { 678 synchronized (mCurrentThumbRequest) { 679 mCurrentThumbRequest.mState = MediaThumbRequest.State.DONE; 680 mCurrentThumbRequest.notifyAll(); 681 } 682 } 683 } 684 } else if (msg.what == ALBUM_THUMB) { 685 ThumbData d; 686 synchronized (mThumbRequestStack) { 687 d = (ThumbData)mThumbRequestStack.pop(); 688 } 689 690 IoUtils.closeQuietly(makeThumbInternal(d)); 691 synchronized (mPendingThumbs) { 692 mPendingThumbs.remove(d.path); 693 } 694 } 695 } 696 }; 697 698 return true; 699 } 700 701 private static final String TABLE_FILES = "files"; 702 private static final String TABLE_ALBUM_ART = "album_art"; 703 private static final String TABLE_THUMBNAILS = "thumbnails"; 704 private static final String TABLE_VIDEO_THUMBNAILS = "videothumbnails"; 705 706 private static final String IMAGE_COLUMNS = 707 "_data,_size,_display_name,mime_type,title,date_added," + 708 "date_modified,description,picasa_id,isprivate,latitude,longitude," + 709 "datetaken,orientation,mini_thumb_magic,bucket_id,bucket_display_name," + 710 "width,height"; 711 712 private static final String IMAGE_COLUMNSv407 = 713 "_data,_size,_display_name,mime_type,title,date_added," + 714 "date_modified,description,picasa_id,isprivate,latitude,longitude," + 715 "datetaken,orientation,mini_thumb_magic,bucket_id,bucket_display_name"; 716 717 private static final String AUDIO_COLUMNSv99 = 718 "_data,_display_name,_size,mime_type,date_added," + 719 "date_modified,title,title_key,duration,artist_id,composer,album_id," + 720 "track,year,is_ringtone,is_music,is_alarm,is_notification,is_podcast," + 721 "bookmark"; 722 723 private static final String AUDIO_COLUMNSv100 = 724 "_data,_display_name,_size,mime_type,date_added," + 725 "date_modified,title,title_key,duration,artist_id,composer,album_id," + 726 "track,year,is_ringtone,is_music,is_alarm,is_notification,is_podcast," + 727 "bookmark,album_artist"; 728 729 private static final String AUDIO_COLUMNSv405 = 730 "_data,_display_name,_size,mime_type,date_added,is_drm," + 731 "date_modified,title,title_key,duration,artist_id,composer,album_id," + 732 "track,year,is_ringtone,is_music,is_alarm,is_notification,is_podcast," + 733 "bookmark,album_artist"; 734 735 private static final String VIDEO_COLUMNS = 736 "_data,_display_name,_size,mime_type,date_added,date_modified," + 737 "title,duration,artist,album,resolution,description,isprivate,tags," + 738 "category,language,mini_thumb_data,latitude,longitude,datetaken," + 739 "mini_thumb_magic,bucket_id,bucket_display_name,bookmark,width," + 740 "height"; 741 742 private static final String VIDEO_COLUMNSv407 = 743 "_data,_display_name,_size,mime_type,date_added,date_modified," + 744 "title,duration,artist,album,resolution,description,isprivate,tags," + 745 "category,language,mini_thumb_data,latitude,longitude,datetaken," + 746 "mini_thumb_magic,bucket_id,bucket_display_name, bookmark"; 747 748 private static final String PLAYLIST_COLUMNS = "_data,name,date_added,date_modified"; 749 750 /** 751 * This method takes care of updating all the tables in the database to the 752 * current version, creating them if necessary. 753 * This method can only update databases at schema 63 or higher, which was 754 * created August 1, 2008. Older database will be cleared and recreated. 755 * @param db Database 756 * @param internal True if this is the internal media database 757 */ updateDatabase(Context context, SQLiteDatabase db, boolean internal, int fromVersion, int toVersion)758 private static void updateDatabase(Context context, SQLiteDatabase db, boolean internal, 759 int fromVersion, int toVersion) { 760 761 // sanity checks 762 int dbversion = getDatabaseVersion(context); 763 if (toVersion != dbversion) { 764 Log.e(TAG, "Illegal update request. Got " + toVersion + ", expected " + dbversion); 765 throw new IllegalArgumentException(); 766 } else if (fromVersion > toVersion) { 767 Log.e(TAG, "Illegal update request: can't downgrade from " + fromVersion + 768 " to " + toVersion + ". Did you forget to wipe data?"); 769 throw new IllegalArgumentException(); 770 } 771 long startTime = SystemClock.currentTimeMicro(); 772 773 // Revisions 84-86 were a failed attempt at supporting the "album artist" id3 tag. 774 // We can't downgrade from those revisions, so start over. 775 // (the initial change to do this was wrong, so now we actually need to start over 776 // if the database version is 84-89) 777 // Post-gingerbread, revisions 91-94 were broken in a way that is not easy to repair. 778 // However version 91 was reused in a divergent development path for gingerbread, 779 // so we need to support upgrades from 91. 780 // Therefore we will only force a reset for versions 92 - 94. 781 if (fromVersion < 63 || (fromVersion >= 84 && fromVersion <= 89) || 782 (fromVersion >= 92 && fromVersion <= 94)) { 783 // Drop everything and start over. 784 Log.i(TAG, "Upgrading media database from version " + 785 fromVersion + " to " + toVersion + ", which will destroy all old data"); 786 fromVersion = 63; 787 db.execSQL("DROP TABLE IF EXISTS images"); 788 db.execSQL("DROP TRIGGER IF EXISTS images_cleanup"); 789 db.execSQL("DROP TABLE IF EXISTS thumbnails"); 790 db.execSQL("DROP TRIGGER IF EXISTS thumbnails_cleanup"); 791 db.execSQL("DROP TABLE IF EXISTS audio_meta"); 792 db.execSQL("DROP TABLE IF EXISTS artists"); 793 db.execSQL("DROP TABLE IF EXISTS albums"); 794 db.execSQL("DROP TABLE IF EXISTS album_art"); 795 db.execSQL("DROP VIEW IF EXISTS artist_info"); 796 db.execSQL("DROP VIEW IF EXISTS album_info"); 797 db.execSQL("DROP VIEW IF EXISTS artists_albums_map"); 798 db.execSQL("DROP TRIGGER IF EXISTS audio_meta_cleanup"); 799 db.execSQL("DROP TABLE IF EXISTS audio_genres"); 800 db.execSQL("DROP TABLE IF EXISTS audio_genres_map"); 801 db.execSQL("DROP TRIGGER IF EXISTS audio_genres_cleanup"); 802 db.execSQL("DROP TABLE IF EXISTS audio_playlists"); 803 db.execSQL("DROP TABLE IF EXISTS audio_playlists_map"); 804 db.execSQL("DROP TRIGGER IF EXISTS audio_playlists_cleanup"); 805 db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup1"); 806 db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup2"); 807 db.execSQL("DROP TABLE IF EXISTS video"); 808 db.execSQL("DROP TRIGGER IF EXISTS video_cleanup"); 809 db.execSQL("DROP TABLE IF EXISTS objects"); 810 db.execSQL("DROP TRIGGER IF EXISTS images_objects_cleanup"); 811 db.execSQL("DROP TRIGGER IF EXISTS audio_objects_cleanup"); 812 db.execSQL("DROP TRIGGER IF EXISTS video_objects_cleanup"); 813 db.execSQL("DROP TRIGGER IF EXISTS playlists_objects_cleanup"); 814 815 db.execSQL("CREATE TABLE IF NOT EXISTS images (" + 816 "_id INTEGER PRIMARY KEY," + 817 "_data TEXT," + 818 "_size INTEGER," + 819 "_display_name TEXT," + 820 "mime_type TEXT," + 821 "title TEXT," + 822 "date_added INTEGER," + 823 "date_modified INTEGER," + 824 "description TEXT," + 825 "picasa_id TEXT," + 826 "isprivate INTEGER," + 827 "latitude DOUBLE," + 828 "longitude DOUBLE," + 829 "datetaken INTEGER," + 830 "orientation INTEGER," + 831 "mini_thumb_magic INTEGER," + 832 "bucket_id TEXT," + 833 "bucket_display_name TEXT" + 834 ");"); 835 836 db.execSQL("CREATE INDEX IF NOT EXISTS mini_thumb_magic_index on images(mini_thumb_magic);"); 837 838 db.execSQL("CREATE TRIGGER IF NOT EXISTS images_cleanup DELETE ON images " + 839 "BEGIN " + 840 "DELETE FROM thumbnails WHERE image_id = old._id;" + 841 "SELECT _DELETE_FILE(old._data);" + 842 "END"); 843 844 // create image thumbnail table 845 db.execSQL("CREATE TABLE IF NOT EXISTS thumbnails (" + 846 "_id INTEGER PRIMARY KEY," + 847 "_data TEXT," + 848 "image_id INTEGER," + 849 "kind INTEGER," + 850 "width INTEGER," + 851 "height INTEGER" + 852 ");"); 853 854 db.execSQL("CREATE INDEX IF NOT EXISTS image_id_index on thumbnails(image_id);"); 855 856 db.execSQL("CREATE TRIGGER IF NOT EXISTS thumbnails_cleanup DELETE ON thumbnails " + 857 "BEGIN " + 858 "SELECT _DELETE_FILE(old._data);" + 859 "END"); 860 861 // Contains meta data about audio files 862 db.execSQL("CREATE TABLE IF NOT EXISTS audio_meta (" + 863 "_id INTEGER PRIMARY KEY," + 864 "_data TEXT UNIQUE NOT NULL," + 865 "_display_name TEXT," + 866 "_size INTEGER," + 867 "mime_type TEXT," + 868 "date_added INTEGER," + 869 "date_modified INTEGER," + 870 "title TEXT NOT NULL," + 871 "title_key TEXT NOT NULL," + 872 "duration INTEGER," + 873 "artist_id INTEGER," + 874 "composer TEXT," + 875 "album_id INTEGER," + 876 "track INTEGER," + // track is an integer to allow proper sorting 877 "year INTEGER CHECK(year!=0)," + 878 "is_ringtone INTEGER," + 879 "is_music INTEGER," + 880 "is_alarm INTEGER," + 881 "is_notification INTEGER" + 882 ");"); 883 884 // Contains a sort/group "key" and the preferred display name for artists 885 db.execSQL("CREATE TABLE IF NOT EXISTS artists (" + 886 "artist_id INTEGER PRIMARY KEY," + 887 "artist_key TEXT NOT NULL UNIQUE," + 888 "artist TEXT NOT NULL" + 889 ");"); 890 891 // Contains a sort/group "key" and the preferred display name for albums 892 db.execSQL("CREATE TABLE IF NOT EXISTS albums (" + 893 "album_id INTEGER PRIMARY KEY," + 894 "album_key TEXT NOT NULL UNIQUE," + 895 "album TEXT NOT NULL" + 896 ");"); 897 898 db.execSQL("CREATE TABLE IF NOT EXISTS album_art (" + 899 "album_id INTEGER PRIMARY KEY," + 900 "_data TEXT" + 901 ");"); 902 903 recreateAudioView(db); 904 905 906 // Provides some extra info about artists, like the number of tracks 907 // and albums for this artist 908 db.execSQL("CREATE VIEW IF NOT EXISTS artist_info AS " + 909 "SELECT artist_id AS _id, artist, artist_key, " + 910 "COUNT(DISTINCT album) AS number_of_albums, " + 911 "COUNT(*) AS number_of_tracks FROM audio WHERE is_music=1 "+ 912 "GROUP BY artist_key;"); 913 914 // Provides extra info albums, such as the number of tracks 915 db.execSQL("CREATE VIEW IF NOT EXISTS album_info AS " + 916 "SELECT audio.album_id AS _id, album, album_key, " + 917 "MIN(year) AS minyear, " + 918 "MAX(year) AS maxyear, artist, artist_id, artist_key, " + 919 "count(*) AS " + MediaStore.Audio.Albums.NUMBER_OF_SONGS + 920 ",album_art._data AS album_art" + 921 " FROM audio LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id" + 922 " WHERE is_music=1 GROUP BY audio.album_id;"); 923 924 // For a given artist_id, provides the album_id for albums on 925 // which the artist appears. 926 db.execSQL("CREATE VIEW IF NOT EXISTS artists_albums_map AS " + 927 "SELECT DISTINCT artist_id, album_id FROM audio_meta;"); 928 929 /* 930 * Only external media volumes can handle genres, playlists, etc. 931 */ 932 if (!internal) { 933 // Cleans up when an audio file is deleted 934 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_meta_cleanup DELETE ON audio_meta " + 935 "BEGIN " + 936 "DELETE FROM audio_genres_map WHERE audio_id = old._id;" + 937 "DELETE FROM audio_playlists_map WHERE audio_id = old._id;" + 938 "END"); 939 940 // Contains audio genre definitions 941 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres (" + 942 "_id INTEGER PRIMARY KEY," + 943 "name TEXT NOT NULL" + 944 ");"); 945 946 // Contains mappings between audio genres and audio files 947 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres_map (" + 948 "_id INTEGER PRIMARY KEY," + 949 "audio_id INTEGER NOT NULL," + 950 "genre_id INTEGER NOT NULL" + 951 ");"); 952 953 // Cleans up when an audio genre is delete 954 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_genres_cleanup DELETE ON audio_genres " + 955 "BEGIN " + 956 "DELETE FROM audio_genres_map WHERE genre_id = old._id;" + 957 "END"); 958 959 // Contains audio playlist definitions 960 db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists (" + 961 "_id INTEGER PRIMARY KEY," + 962 "_data TEXT," + // _data is path for file based playlists, or null 963 "name TEXT NOT NULL," + 964 "date_added INTEGER," + 965 "date_modified INTEGER" + 966 ");"); 967 968 // Contains mappings between audio playlists and audio files 969 db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists_map (" + 970 "_id INTEGER PRIMARY KEY," + 971 "audio_id INTEGER NOT NULL," + 972 "playlist_id INTEGER NOT NULL," + 973 "play_order INTEGER NOT NULL" + 974 ");"); 975 976 // Cleans up when an audio playlist is deleted 977 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_playlists_cleanup DELETE ON audio_playlists " + 978 "BEGIN " + 979 "DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" + 980 "SELECT _DELETE_FILE(old._data);" + 981 "END"); 982 983 // Cleans up album_art table entry when an album is deleted 984 db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup1 DELETE ON albums " + 985 "BEGIN " + 986 "DELETE FROM album_art WHERE album_id = old.album_id;" + 987 "END"); 988 989 // Cleans up album_art when an album is deleted 990 db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup2 DELETE ON album_art " + 991 "BEGIN " + 992 "SELECT _DELETE_FILE(old._data);" + 993 "END"); 994 } 995 996 // Contains meta data about video files 997 db.execSQL("CREATE TABLE IF NOT EXISTS video (" + 998 "_id INTEGER PRIMARY KEY," + 999 "_data TEXT NOT NULL," + 1000 "_display_name TEXT," + 1001 "_size INTEGER," + 1002 "mime_type TEXT," + 1003 "date_added INTEGER," + 1004 "date_modified INTEGER," + 1005 "title TEXT," + 1006 "duration INTEGER," + 1007 "artist TEXT," + 1008 "album TEXT," + 1009 "resolution TEXT," + 1010 "description TEXT," + 1011 "isprivate INTEGER," + // for YouTube videos 1012 "tags TEXT," + // for YouTube videos 1013 "category TEXT," + // for YouTube videos 1014 "language TEXT," + // for YouTube videos 1015 "mini_thumb_data TEXT," + 1016 "latitude DOUBLE," + 1017 "longitude DOUBLE," + 1018 "datetaken INTEGER," + 1019 "mini_thumb_magic INTEGER" + 1020 ");"); 1021 1022 db.execSQL("CREATE TRIGGER IF NOT EXISTS video_cleanup DELETE ON video " + 1023 "BEGIN " + 1024 "SELECT _DELETE_FILE(old._data);" + 1025 "END"); 1026 } 1027 1028 // At this point the database is at least at schema version 63 (it was 1029 // either created at version 63 by the code above, or was already at 1030 // version 63 or later) 1031 1032 if (fromVersion < 64) { 1033 // create the index that updates the database to schema version 64 1034 db.execSQL("CREATE INDEX IF NOT EXISTS sort_index on images(datetaken ASC, _id ASC);"); 1035 } 1036 1037 /* 1038 * Android 1.0 shipped with database version 64 1039 */ 1040 1041 if (fromVersion < 65) { 1042 // create the index that updates the database to schema version 65 1043 db.execSQL("CREATE INDEX IF NOT EXISTS titlekey_index on audio_meta(title_key);"); 1044 } 1045 1046 // In version 66, originally we updateBucketNames(db, "images"), 1047 // but we need to do it in version 89 and therefore save the update here. 1048 1049 if (fromVersion < 67) { 1050 // create the indices that update the database to schema version 67 1051 db.execSQL("CREATE INDEX IF NOT EXISTS albumkey_index on albums(album_key);"); 1052 db.execSQL("CREATE INDEX IF NOT EXISTS artistkey_index on artists(artist_key);"); 1053 } 1054 1055 if (fromVersion < 68) { 1056 // Create bucket_id and bucket_display_name columns for the video table. 1057 db.execSQL("ALTER TABLE video ADD COLUMN bucket_id TEXT;"); 1058 db.execSQL("ALTER TABLE video ADD COLUMN bucket_display_name TEXT"); 1059 1060 // In version 68, originally we updateBucketNames(db, "video"), 1061 // but we need to do it in version 89 and therefore save the update here. 1062 } 1063 1064 if (fromVersion < 69) { 1065 updateDisplayName(db, "images"); 1066 } 1067 1068 if (fromVersion < 70) { 1069 // Create bookmark column for the video table. 1070 db.execSQL("ALTER TABLE video ADD COLUMN bookmark INTEGER;"); 1071 } 1072 1073 if (fromVersion < 71) { 1074 // There is no change to the database schema, however a code change 1075 // fixed parsing of metadata for certain files bought from the 1076 // iTunes music store, so we want to rescan files that might need it. 1077 // We do this by clearing the modification date in the database for 1078 // those files, so that the media scanner will see them as updated 1079 // and rescan them. 1080 db.execSQL("UPDATE audio_meta SET date_modified=0 WHERE _id IN (" + 1081 "SELECT _id FROM audio where mime_type='audio/mp4' AND " + 1082 "artist='" + MediaStore.UNKNOWN_STRING + "' AND " + 1083 "album='" + MediaStore.UNKNOWN_STRING + "'" + 1084 ");"); 1085 } 1086 1087 if (fromVersion < 72) { 1088 // Create is_podcast and bookmark columns for the audio table. 1089 db.execSQL("ALTER TABLE audio_meta ADD COLUMN is_podcast INTEGER;"); 1090 db.execSQL("UPDATE audio_meta SET is_podcast=1 WHERE _data LIKE '%/podcasts/%';"); 1091 db.execSQL("UPDATE audio_meta SET is_music=0 WHERE is_podcast=1" + 1092 " AND _data NOT LIKE '%/music/%';"); 1093 db.execSQL("ALTER TABLE audio_meta ADD COLUMN bookmark INTEGER;"); 1094 1095 // New columns added to tables aren't visible in views on those tables 1096 // without opening and closing the database (or using the 'vacuum' command, 1097 // which we can't do here because all this code runs inside a transaction). 1098 // To work around this, we drop and recreate the affected view and trigger. 1099 recreateAudioView(db); 1100 } 1101 1102 /* 1103 * Android 1.5 shipped with database version 72 1104 */ 1105 1106 if (fromVersion < 73) { 1107 // There is no change to the database schema, but we now do case insensitive 1108 // matching of folder names when determining whether something is music, a 1109 // ringtone, podcast, etc, so we might need to reclassify some files. 1110 db.execSQL("UPDATE audio_meta SET is_music=1 WHERE is_music=0 AND " + 1111 "_data LIKE '%/music/%';"); 1112 db.execSQL("UPDATE audio_meta SET is_ringtone=1 WHERE is_ringtone=0 AND " + 1113 "_data LIKE '%/ringtones/%';"); 1114 db.execSQL("UPDATE audio_meta SET is_notification=1 WHERE is_notification=0 AND " + 1115 "_data LIKE '%/notifications/%';"); 1116 db.execSQL("UPDATE audio_meta SET is_alarm=1 WHERE is_alarm=0 AND " + 1117 "_data LIKE '%/alarms/%';"); 1118 db.execSQL("UPDATE audio_meta SET is_podcast=1 WHERE is_podcast=0 AND " + 1119 "_data LIKE '%/podcasts/%';"); 1120 } 1121 1122 if (fromVersion < 74) { 1123 // This view is used instead of the audio view by the union below, to force 1124 // sqlite to use the title_key index. This greatly reduces memory usage 1125 // (no separate copy pass needed for sorting, which could cause errors on 1126 // large datasets) and improves speed (by about 35% on a large dataset) 1127 db.execSQL("CREATE VIEW IF NOT EXISTS searchhelpertitle AS SELECT * FROM audio " + 1128 "ORDER BY title_key;"); 1129 1130 db.execSQL("CREATE VIEW IF NOT EXISTS search AS " + 1131 "SELECT _id," + 1132 "'artist' AS mime_type," + 1133 "artist," + 1134 "NULL AS album," + 1135 "NULL AS title," + 1136 "artist AS text1," + 1137 "NULL AS text2," + 1138 "number_of_albums AS data1," + 1139 "number_of_tracks AS data2," + 1140 "artist_key AS match," + 1141 "'content://media/external/audio/artists/'||_id AS suggest_intent_data," + 1142 "1 AS grouporder " + 1143 "FROM artist_info WHERE (artist!='" + MediaStore.UNKNOWN_STRING + "') " + 1144 "UNION ALL " + 1145 "SELECT _id," + 1146 "'album' AS mime_type," + 1147 "artist," + 1148 "album," + 1149 "NULL AS title," + 1150 "album AS text1," + 1151 "artist AS text2," + 1152 "NULL AS data1," + 1153 "NULL AS data2," + 1154 "artist_key||' '||album_key AS match," + 1155 "'content://media/external/audio/albums/'||_id AS suggest_intent_data," + 1156 "2 AS grouporder " + 1157 "FROM album_info WHERE (album!='" + MediaStore.UNKNOWN_STRING + "') " + 1158 "UNION ALL " + 1159 "SELECT searchhelpertitle._id AS _id," + 1160 "mime_type," + 1161 "artist," + 1162 "album," + 1163 "title," + 1164 "title AS text1," + 1165 "artist AS text2," + 1166 "NULL AS data1," + 1167 "NULL AS data2," + 1168 "artist_key||' '||album_key||' '||title_key AS match," + 1169 "'content://media/external/audio/media/'||searchhelpertitle._id AS " + 1170 "suggest_intent_data," + 1171 "3 AS grouporder " + 1172 "FROM searchhelpertitle WHERE (title != '') " 1173 ); 1174 } 1175 1176 if (fromVersion < 75) { 1177 // Force a rescan of the audio entries so we can apply the new logic to 1178 // distinguish same-named albums. 1179 db.execSQL("UPDATE audio_meta SET date_modified=0;"); 1180 db.execSQL("DELETE FROM albums"); 1181 } 1182 1183 if (fromVersion < 76) { 1184 // We now ignore double quotes when building the key, so we have to remove all of them 1185 // from existing keys. 1186 db.execSQL("UPDATE audio_meta SET title_key=" + 1187 "REPLACE(title_key,x'081D08C29F081D',x'081D') " + 1188 "WHERE title_key LIKE '%'||x'081D08C29F081D'||'%';"); 1189 db.execSQL("UPDATE albums SET album_key=" + 1190 "REPLACE(album_key,x'081D08C29F081D',x'081D') " + 1191 "WHERE album_key LIKE '%'||x'081D08C29F081D'||'%';"); 1192 db.execSQL("UPDATE artists SET artist_key=" + 1193 "REPLACE(artist_key,x'081D08C29F081D',x'081D') " + 1194 "WHERE artist_key LIKE '%'||x'081D08C29F081D'||'%';"); 1195 } 1196 1197 /* 1198 * Android 1.6 shipped with database version 76 1199 */ 1200 1201 if (fromVersion < 77) { 1202 // create video thumbnail table 1203 db.execSQL("CREATE TABLE IF NOT EXISTS videothumbnails (" + 1204 "_id INTEGER PRIMARY KEY," + 1205 "_data TEXT," + 1206 "video_id INTEGER," + 1207 "kind INTEGER," + 1208 "width INTEGER," + 1209 "height INTEGER" + 1210 ");"); 1211 1212 db.execSQL("CREATE INDEX IF NOT EXISTS video_id_index on videothumbnails(video_id);"); 1213 1214 db.execSQL("CREATE TRIGGER IF NOT EXISTS videothumbnails_cleanup DELETE ON videothumbnails " + 1215 "BEGIN " + 1216 "SELECT _DELETE_FILE(old._data);" + 1217 "END"); 1218 } 1219 1220 /* 1221 * Android 2.0 and 2.0.1 shipped with database version 77 1222 */ 1223 1224 if (fromVersion < 78) { 1225 // Force a rescan of the video entries so we can update 1226 // latest changed DATE_TAKEN units (in milliseconds). 1227 db.execSQL("UPDATE video SET date_modified=0;"); 1228 } 1229 1230 /* 1231 * Android 2.1 shipped with database version 78 1232 */ 1233 1234 if (fromVersion < 79) { 1235 // move /sdcard/albumthumbs to 1236 // /sdcard/Android/data/com.android.providers.media/albumthumbs, 1237 // and update the database accordingly 1238 1239 final StorageManager sm = context.getSystemService(StorageManager.class); 1240 final StorageVolume vol = sm.getPrimaryVolume(); 1241 1242 String oldthumbspath = vol.getPath() + "/albumthumbs"; 1243 String newthumbspath = vol.getPath() + "/" + ALBUM_THUMB_FOLDER; 1244 File thumbsfolder = new File(oldthumbspath); 1245 if (thumbsfolder.exists()) { 1246 // move folder to its new location 1247 File newthumbsfolder = new File(newthumbspath); 1248 newthumbsfolder.getParentFile().mkdirs(); 1249 if(thumbsfolder.renameTo(newthumbsfolder)) { 1250 // update the database 1251 db.execSQL("UPDATE album_art SET _data=REPLACE(_data, '" + 1252 oldthumbspath + "','" + newthumbspath + "');"); 1253 } 1254 } 1255 } 1256 1257 if (fromVersion < 80) { 1258 // Force rescan of image entries to update DATE_TAKEN as UTC timestamp. 1259 db.execSQL("UPDATE images SET date_modified=0;"); 1260 } 1261 1262 if (fromVersion < 81 && !internal) { 1263 // Delete entries starting with /mnt/sdcard. This is for the benefit 1264 // of users running builds between 2.0.1 and 2.1 final only, since 1265 // users updating from 2.0 or earlier will not have such entries. 1266 1267 // First we need to update the _data fields in the affected tables, since 1268 // otherwise deleting the entries will also delete the underlying files 1269 // (via a trigger), and we want to keep them. 1270 db.execSQL("UPDATE audio_playlists SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1271 db.execSQL("UPDATE images SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1272 db.execSQL("UPDATE video SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1273 db.execSQL("UPDATE videothumbnails SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1274 db.execSQL("UPDATE thumbnails SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1275 db.execSQL("UPDATE album_art SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1276 db.execSQL("UPDATE audio_meta SET _data='////' WHERE _data LIKE '/mnt/sdcard/%';"); 1277 // Once the paths have been renamed, we can safely delete the entries 1278 db.execSQL("DELETE FROM audio_playlists WHERE _data IS '////';"); 1279 db.execSQL("DELETE FROM images WHERE _data IS '////';"); 1280 db.execSQL("DELETE FROM video WHERE _data IS '////';"); 1281 db.execSQL("DELETE FROM videothumbnails WHERE _data IS '////';"); 1282 db.execSQL("DELETE FROM thumbnails WHERE _data IS '////';"); 1283 db.execSQL("DELETE FROM audio_meta WHERE _data IS '////';"); 1284 db.execSQL("DELETE FROM album_art WHERE _data IS '////';"); 1285 1286 // rename existing entries starting with /sdcard to /mnt/sdcard 1287 db.execSQL("UPDATE audio_meta" + 1288 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1289 db.execSQL("UPDATE audio_playlists" + 1290 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1291 db.execSQL("UPDATE images" + 1292 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1293 db.execSQL("UPDATE video" + 1294 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1295 db.execSQL("UPDATE videothumbnails" + 1296 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1297 db.execSQL("UPDATE thumbnails" + 1298 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1299 db.execSQL("UPDATE album_art" + 1300 " SET _data='/mnt/sdcard'||SUBSTR(_data,8) WHERE _data LIKE '/sdcard/%';"); 1301 1302 // Delete albums and artists, then clear the modification time on songs, which 1303 // will cause the media scanner to rescan everything, rebuilding the artist and 1304 // album tables along the way, while preserving playlists. 1305 // We need this rescan because ICU also changed, and now generates different 1306 // collation keys 1307 db.execSQL("DELETE from albums"); 1308 db.execSQL("DELETE from artists"); 1309 db.execSQL("UPDATE audio_meta SET date_modified=0;"); 1310 } 1311 1312 if (fromVersion < 82) { 1313 // recreate this view with the correct "group by" specifier 1314 db.execSQL("DROP VIEW IF EXISTS artist_info"); 1315 db.execSQL("CREATE VIEW IF NOT EXISTS artist_info AS " + 1316 "SELECT artist_id AS _id, artist, artist_key, " + 1317 "COUNT(DISTINCT album_key) AS number_of_albums, " + 1318 "COUNT(*) AS number_of_tracks FROM audio WHERE is_music=1 "+ 1319 "GROUP BY artist_key;"); 1320 } 1321 1322 /* we skipped over version 83, and reverted versions 84, 85 and 86 */ 1323 1324 if (fromVersion < 87) { 1325 // The fastscroll thumb needs an index on the strings being displayed, 1326 // otherwise the queries it does to determine the correct position 1327 // becomes really inefficient 1328 db.execSQL("CREATE INDEX IF NOT EXISTS title_idx on audio_meta(title);"); 1329 db.execSQL("CREATE INDEX IF NOT EXISTS artist_idx on artists(artist);"); 1330 db.execSQL("CREATE INDEX IF NOT EXISTS album_idx on albums(album);"); 1331 } 1332 1333 if (fromVersion < 88) { 1334 // Clean up a few more things from versions 84/85/86, and recreate 1335 // the few things worth keeping from those changes. 1336 db.execSQL("DROP TRIGGER IF EXISTS albums_update1;"); 1337 db.execSQL("DROP TRIGGER IF EXISTS albums_update2;"); 1338 db.execSQL("DROP TRIGGER IF EXISTS albums_update3;"); 1339 db.execSQL("DROP TRIGGER IF EXISTS albums_update4;"); 1340 db.execSQL("DROP TRIGGER IF EXISTS artist_update1;"); 1341 db.execSQL("DROP TRIGGER IF EXISTS artist_update2;"); 1342 db.execSQL("DROP TRIGGER IF EXISTS artist_update3;"); 1343 db.execSQL("DROP TRIGGER IF EXISTS artist_update4;"); 1344 db.execSQL("DROP VIEW IF EXISTS album_artists;"); 1345 db.execSQL("CREATE INDEX IF NOT EXISTS album_id_idx on audio_meta(album_id);"); 1346 db.execSQL("CREATE INDEX IF NOT EXISTS artist_id_idx on audio_meta(artist_id);"); 1347 // For a given artist_id, provides the album_id for albums on 1348 // which the artist appears. 1349 db.execSQL("CREATE VIEW IF NOT EXISTS artists_albums_map AS " + 1350 "SELECT DISTINCT artist_id, album_id FROM audio_meta;"); 1351 } 1352 1353 // In version 89, originally we updateBucketNames(db, "images") and 1354 // updateBucketNames(db, "video"), but in version 101 we now updateBucketNames 1355 // for all files and therefore can save the update here. 1356 1357 if (fromVersion < 91) { 1358 // Never query by mini_thumb_magic_index 1359 db.execSQL("DROP INDEX IF EXISTS mini_thumb_magic_index"); 1360 1361 // sort the items by taken date in each bucket 1362 db.execSQL("CREATE INDEX IF NOT EXISTS image_bucket_index ON images(bucket_id, datetaken)"); 1363 db.execSQL("CREATE INDEX IF NOT EXISTS video_bucket_index ON video(bucket_id, datetaken)"); 1364 } 1365 1366 1367 // Gingerbread ended up going to version 100, but didn't yet have the "files" 1368 // table, so we need to create that if we're at 100 or lower. This means 1369 // we won't be able to upgrade pre-release Honeycomb. 1370 if (fromVersion <= 100) { 1371 // Remove various stages of work in progress for MTP support 1372 db.execSQL("DROP TABLE IF EXISTS objects"); 1373 db.execSQL("DROP TABLE IF EXISTS files"); 1374 db.execSQL("DROP TRIGGER IF EXISTS images_objects_cleanup;"); 1375 db.execSQL("DROP TRIGGER IF EXISTS audio_objects_cleanup;"); 1376 db.execSQL("DROP TRIGGER IF EXISTS video_objects_cleanup;"); 1377 db.execSQL("DROP TRIGGER IF EXISTS playlists_objects_cleanup;"); 1378 db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_images;"); 1379 db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_audio;"); 1380 db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_video;"); 1381 db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_playlists;"); 1382 db.execSQL("DROP TRIGGER IF EXISTS media_cleanup;"); 1383 1384 // Create a new table to manage all files in our storage. 1385 // This contains a union of all the columns from the old 1386 // images, audio_meta, videos and audio_playlist tables. 1387 db.execSQL("CREATE TABLE files (" + 1388 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 1389 "_data TEXT," + // this can be null for playlists 1390 "_size INTEGER," + 1391 "format INTEGER," + 1392 "parent INTEGER," + 1393 "date_added INTEGER," + 1394 "date_modified INTEGER," + 1395 "mime_type TEXT," + 1396 "title TEXT," + 1397 "description TEXT," + 1398 "_display_name TEXT," + 1399 1400 // for images 1401 "picasa_id TEXT," + 1402 "orientation INTEGER," + 1403 1404 // for images and video 1405 "latitude DOUBLE," + 1406 "longitude DOUBLE," + 1407 "datetaken INTEGER," + 1408 "mini_thumb_magic INTEGER," + 1409 "bucket_id TEXT," + 1410 "bucket_display_name TEXT," + 1411 "isprivate INTEGER," + 1412 1413 // for audio 1414 "title_key TEXT," + 1415 "artist_id INTEGER," + 1416 "album_id INTEGER," + 1417 "composer TEXT," + 1418 "track INTEGER," + 1419 "year INTEGER CHECK(year!=0)," + 1420 "is_ringtone INTEGER," + 1421 "is_music INTEGER," + 1422 "is_alarm INTEGER," + 1423 "is_notification INTEGER," + 1424 "is_podcast INTEGER," + 1425 "album_artist TEXT," + 1426 1427 // for audio and video 1428 "duration INTEGER," + 1429 "bookmark INTEGER," + 1430 1431 // for video 1432 "artist TEXT," + 1433 "album TEXT," + 1434 "resolution TEXT," + 1435 "tags TEXT," + 1436 "category TEXT," + 1437 "language TEXT," + 1438 "mini_thumb_data TEXT," + 1439 1440 // for playlists 1441 "name TEXT," + 1442 1443 // media_type is used by the views to emulate the old 1444 // images, audio_meta, videos and audio_playlist tables. 1445 "media_type INTEGER," + 1446 1447 // Value of _id from the old media table. 1448 // Used only for updating other tables during database upgrade. 1449 "old_id INTEGER" + 1450 ");"); 1451 1452 db.execSQL("CREATE INDEX path_index ON files(_data);"); 1453 db.execSQL("CREATE INDEX media_type_index ON files(media_type);"); 1454 1455 // Copy all data from our obsolete tables to the new files table 1456 1457 // Copy audio records first, preserving the _id column. 1458 // We do this to maintain compatibility for content Uris for ringtones. 1459 // Unfortunately we cannot do this for images and videos as well. 1460 // We choose to do this for the audio table because the fragility of Uris 1461 // for ringtones are the most common problem we need to avoid. 1462 db.execSQL("INSERT INTO files (_id," + AUDIO_COLUMNSv99 + ",old_id,media_type)" + 1463 " SELECT _id," + AUDIO_COLUMNSv99 + ",_id," + FileColumns.MEDIA_TYPE_AUDIO + 1464 " FROM audio_meta;"); 1465 1466 db.execSQL("INSERT INTO files (" + IMAGE_COLUMNSv407 + ",old_id,media_type) SELECT " 1467 + IMAGE_COLUMNSv407 + ",_id," + FileColumns.MEDIA_TYPE_IMAGE + " FROM images;"); 1468 db.execSQL("INSERT INTO files (" + VIDEO_COLUMNSv407 + ",old_id,media_type) SELECT " 1469 + VIDEO_COLUMNSv407 + ",_id," + FileColumns.MEDIA_TYPE_VIDEO + " FROM video;"); 1470 if (!internal) { 1471 db.execSQL("INSERT INTO files (" + PLAYLIST_COLUMNS + ",old_id,media_type) SELECT " 1472 + PLAYLIST_COLUMNS + ",_id," + FileColumns.MEDIA_TYPE_PLAYLIST 1473 + " FROM audio_playlists;"); 1474 } 1475 1476 // Delete the old tables 1477 db.execSQL("DROP TABLE IF EXISTS images"); 1478 db.execSQL("DROP TABLE IF EXISTS audio_meta"); 1479 db.execSQL("DROP TABLE IF EXISTS video"); 1480 db.execSQL("DROP TABLE IF EXISTS audio_playlists"); 1481 1482 // Create views to replace our old tables 1483 db.execSQL("CREATE VIEW images AS SELECT _id," + IMAGE_COLUMNSv407 + 1484 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1485 + FileColumns.MEDIA_TYPE_IMAGE + ";"); 1486 db.execSQL("CREATE VIEW audio_meta AS SELECT _id," + AUDIO_COLUMNSv100 + 1487 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1488 + FileColumns.MEDIA_TYPE_AUDIO + ";"); 1489 db.execSQL("CREATE VIEW video AS SELECT _id," + VIDEO_COLUMNSv407 + 1490 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1491 + FileColumns.MEDIA_TYPE_VIDEO + ";"); 1492 if (!internal) { 1493 db.execSQL("CREATE VIEW audio_playlists AS SELECT _id," + PLAYLIST_COLUMNS + 1494 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1495 + FileColumns.MEDIA_TYPE_PLAYLIST + ";"); 1496 } 1497 1498 // create temporary index to make the updates go faster 1499 db.execSQL("CREATE INDEX tmp ON files(old_id);"); 1500 1501 // update the image_id column in the thumbnails table. 1502 db.execSQL("UPDATE thumbnails SET image_id = (SELECT _id FROM files " 1503 + "WHERE files.old_id = thumbnails.image_id AND files.media_type = " 1504 + FileColumns.MEDIA_TYPE_IMAGE + ");"); 1505 1506 if (!internal) { 1507 // update audio_id in the audio_genres_map table, and 1508 // audio_playlists_map tables and playlist_id in the audio_playlists_map table 1509 db.execSQL("UPDATE audio_genres_map SET audio_id = (SELECT _id FROM files " 1510 + "WHERE files.old_id = audio_genres_map.audio_id AND files.media_type = " 1511 + FileColumns.MEDIA_TYPE_AUDIO + ");"); 1512 db.execSQL("UPDATE audio_playlists_map SET audio_id = (SELECT _id FROM files " 1513 + "WHERE files.old_id = audio_playlists_map.audio_id " 1514 + "AND files.media_type = " + FileColumns.MEDIA_TYPE_AUDIO + ");"); 1515 db.execSQL("UPDATE audio_playlists_map SET playlist_id = (SELECT _id FROM files " 1516 + "WHERE files.old_id = audio_playlists_map.playlist_id " 1517 + "AND files.media_type = " + FileColumns.MEDIA_TYPE_PLAYLIST + ");"); 1518 } 1519 1520 // update video_id in the videothumbnails table. 1521 db.execSQL("UPDATE videothumbnails SET video_id = (SELECT _id FROM files " 1522 + "WHERE files.old_id = videothumbnails.video_id AND files.media_type = " 1523 + FileColumns.MEDIA_TYPE_VIDEO + ");"); 1524 1525 // we don't need this index anymore now 1526 db.execSQL("DROP INDEX tmp;"); 1527 1528 // update indices to work on the files table 1529 db.execSQL("DROP INDEX IF EXISTS title_idx"); 1530 db.execSQL("DROP INDEX IF EXISTS album_id_idx"); 1531 db.execSQL("DROP INDEX IF EXISTS image_bucket_index"); 1532 db.execSQL("DROP INDEX IF EXISTS video_bucket_index"); 1533 db.execSQL("DROP INDEX IF EXISTS sort_index"); 1534 db.execSQL("DROP INDEX IF EXISTS titlekey_index"); 1535 db.execSQL("DROP INDEX IF EXISTS artist_id_idx"); 1536 db.execSQL("CREATE INDEX title_idx ON files(title);"); 1537 db.execSQL("CREATE INDEX album_id_idx ON files(album_id);"); 1538 db.execSQL("CREATE INDEX bucket_index ON files(bucket_id, datetaken);"); 1539 db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC);"); 1540 db.execSQL("CREATE INDEX titlekey_index ON files(title_key);"); 1541 db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id);"); 1542 1543 // Recreate triggers for our obsolete tables on the new files table 1544 db.execSQL("DROP TRIGGER IF EXISTS images_cleanup"); 1545 db.execSQL("DROP TRIGGER IF EXISTS audio_meta_cleanup"); 1546 db.execSQL("DROP TRIGGER IF EXISTS video_cleanup"); 1547 db.execSQL("DROP TRIGGER IF EXISTS audio_playlists_cleanup"); 1548 db.execSQL("DROP TRIGGER IF EXISTS audio_delete"); 1549 1550 db.execSQL("CREATE TRIGGER IF NOT EXISTS images_cleanup DELETE ON files " + 1551 "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_IMAGE + " " + 1552 "BEGIN " + 1553 "DELETE FROM thumbnails WHERE image_id = old._id;" + 1554 "SELECT _DELETE_FILE(old._data);" + 1555 "END"); 1556 1557 db.execSQL("CREATE TRIGGER IF NOT EXISTS video_cleanup DELETE ON files " + 1558 "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_VIDEO + " " + 1559 "BEGIN " + 1560 "SELECT _DELETE_FILE(old._data);" + 1561 "END"); 1562 1563 if (!internal) { 1564 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_meta_cleanup DELETE ON files " + 1565 "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_AUDIO + " " + 1566 "BEGIN " + 1567 "DELETE FROM audio_genres_map WHERE audio_id = old._id;" + 1568 "DELETE FROM audio_playlists_map WHERE audio_id = old._id;" + 1569 "END"); 1570 1571 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_playlists_cleanup DELETE ON files " + 1572 "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_PLAYLIST + " " + 1573 "BEGIN " + 1574 "DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" + 1575 "SELECT _DELETE_FILE(old._data);" + 1576 "END"); 1577 1578 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_delete INSTEAD OF DELETE ON audio " + 1579 "BEGIN " + 1580 "DELETE from files where _id=old._id;" + 1581 "DELETE from audio_playlists_map where audio_id=old._id;" + 1582 "DELETE from audio_genres_map where audio_id=old._id;" + 1583 "END"); 1584 } 1585 } 1586 1587 if (fromVersion < 301) { 1588 db.execSQL("DROP INDEX IF EXISTS bucket_index"); 1589 db.execSQL("CREATE INDEX bucket_index on files(bucket_id, media_type, datetaken, _id)"); 1590 db.execSQL("CREATE INDEX bucket_name on files(bucket_id, media_type, bucket_display_name)"); 1591 } 1592 1593 if (fromVersion < 302) { 1594 db.execSQL("CREATE INDEX parent_index ON files(parent);"); 1595 db.execSQL("CREATE INDEX format_index ON files(format);"); 1596 } 1597 1598 if (fromVersion < 303) { 1599 // the album disambiguator hash changed, so rescan songs and force 1600 // albums to be updated. Artists are unaffected. 1601 db.execSQL("DELETE from albums"); 1602 db.execSQL("UPDATE files SET date_modified=0 WHERE " + FileColumns.MEDIA_TYPE + "=" 1603 + FileColumns.MEDIA_TYPE_AUDIO + ";"); 1604 } 1605 1606 if (fromVersion < 304 && !internal) { 1607 // notifies host when files are deleted 1608 db.execSQL("CREATE TRIGGER IF NOT EXISTS files_cleanup DELETE ON files " + 1609 "BEGIN " + 1610 "SELECT _OBJECT_REMOVED(old._id);" + 1611 "END"); 1612 1613 } 1614 1615 if (fromVersion < 305 && internal) { 1616 // version 304 erroneously added this trigger to the internal database 1617 db.execSQL("DROP TRIGGER IF EXISTS files_cleanup"); 1618 } 1619 1620 if (fromVersion < 306 && !internal) { 1621 // The genre list was expanded and genre string parsing was tweaked, so 1622 // rebuild the genre list 1623 db.execSQL("UPDATE files SET date_modified=0 WHERE " + FileColumns.MEDIA_TYPE + "=" 1624 + FileColumns.MEDIA_TYPE_AUDIO + ";"); 1625 db.execSQL("DELETE FROM audio_genres_map"); 1626 db.execSQL("DELETE FROM audio_genres"); 1627 } 1628 1629 if (fromVersion < 307 && !internal) { 1630 // Force rescan of image entries to update DATE_TAKEN by either GPSTimeStamp or 1631 // EXIF local time. 1632 db.execSQL("UPDATE files SET date_modified=0 WHERE " + FileColumns.MEDIA_TYPE + "=" 1633 + FileColumns.MEDIA_TYPE_IMAGE + ";"); 1634 } 1635 1636 // Honeycomb went up to version 307, ICS started at 401 1637 1638 // Database version 401 did not add storage_id to the internal database. 1639 // We need it there too, so add it in version 402 1640 if (fromVersion < 401 || (fromVersion == 401 && internal)) { 1641 // Add column for MTP storage ID 1642 db.execSQL("ALTER TABLE files ADD COLUMN storage_id INTEGER;"); 1643 // Anything in the database before this upgrade step will be in the primary storage 1644 db.execSQL("UPDATE files SET storage_id=" + StorageVolume.STORAGE_ID_PRIMARY + ";"); 1645 } 1646 1647 if (fromVersion < 403 && !internal) { 1648 db.execSQL("CREATE VIEW audio_genres_map_noid AS " + 1649 "SELECT audio_id,genre_id from audio_genres_map;"); 1650 } 1651 1652 if (fromVersion < 404) { 1653 // There was a bug that could cause distinct same-named albums to be 1654 // combined again. Delete albums and force a rescan. 1655 db.execSQL("DELETE from albums"); 1656 db.execSQL("UPDATE files SET date_modified=0 WHERE " + FileColumns.MEDIA_TYPE + "=" 1657 + FileColumns.MEDIA_TYPE_AUDIO + ";"); 1658 } 1659 1660 if (fromVersion < 405) { 1661 // Add is_drm column. 1662 db.execSQL("ALTER TABLE files ADD COLUMN is_drm INTEGER;"); 1663 1664 db.execSQL("DROP VIEW IF EXISTS audio_meta"); 1665 db.execSQL("CREATE VIEW audio_meta AS SELECT _id," + AUDIO_COLUMNSv405 + 1666 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1667 + FileColumns.MEDIA_TYPE_AUDIO + ";"); 1668 1669 recreateAudioView(db); 1670 } 1671 1672 if (fromVersion < 407) { 1673 // Rescan files in the media database because a new column has been added 1674 // in table files in version 405 and to recover from problems populating 1675 // the genre tables 1676 db.execSQL("UPDATE files SET date_modified=0;"); 1677 } 1678 1679 if (fromVersion < 408) { 1680 // Add the width/height columns for images and video 1681 db.execSQL("ALTER TABLE files ADD COLUMN width INTEGER;"); 1682 db.execSQL("ALTER TABLE files ADD COLUMN height INTEGER;"); 1683 1684 // Rescan files to fill the columns 1685 db.execSQL("UPDATE files SET date_modified=0;"); 1686 1687 // Update images and video views to contain the width/height columns 1688 db.execSQL("DROP VIEW IF EXISTS images"); 1689 db.execSQL("DROP VIEW IF EXISTS video"); 1690 db.execSQL("CREATE VIEW images AS SELECT _id," + IMAGE_COLUMNS + 1691 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1692 + FileColumns.MEDIA_TYPE_IMAGE + ";"); 1693 db.execSQL("CREATE VIEW video AS SELECT _id," + VIDEO_COLUMNS + 1694 " FROM files WHERE " + FileColumns.MEDIA_TYPE + "=" 1695 + FileColumns.MEDIA_TYPE_VIDEO + ";"); 1696 } 1697 1698 if (fromVersion < 409 && !internal) { 1699 // A bug that prevented numeric genres from being parsed was fixed, so 1700 // rebuild the genre list 1701 db.execSQL("UPDATE files SET date_modified=0 WHERE " + FileColumns.MEDIA_TYPE + "=" 1702 + FileColumns.MEDIA_TYPE_AUDIO + ";"); 1703 db.execSQL("DELETE FROM audio_genres_map"); 1704 db.execSQL("DELETE FROM audio_genres"); 1705 } 1706 1707 // ICS went out with database version 409, JB started at 500 1708 1709 if (fromVersion < 500) { 1710 // we're now deleting the file in mediaprovider code, rather than via a trigger 1711 db.execSQL("DROP TRIGGER IF EXISTS videothumbnails_cleanup;"); 1712 } 1713 if (fromVersion < 501) { 1714 // we're now deleting the file in mediaprovider code, rather than via a trigger 1715 // the images_cleanup trigger would delete the image file and the entry 1716 // in the thumbnail table, which in turn would trigger thumbnails_cleanup 1717 // to delete the thumbnail image 1718 db.execSQL("DROP TRIGGER IF EXISTS images_cleanup;"); 1719 db.execSQL("DROP TRIGGER IF EXISTS thumbnails_cleanup;"); 1720 } 1721 if (fromVersion < 502) { 1722 // we're now deleting the file in mediaprovider code, rather than via a trigger 1723 db.execSQL("DROP TRIGGER IF EXISTS video_cleanup;"); 1724 } 1725 if (fromVersion < 503) { 1726 // genre and playlist cleanup now done in mediaprovider code, instead of in a trigger 1727 db.execSQL("DROP TRIGGER IF EXISTS audio_delete"); 1728 db.execSQL("DROP TRIGGER IF EXISTS audio_meta_cleanup"); 1729 } 1730 if (fromVersion < 504) { 1731 // add an index to help with case-insensitive matching of paths 1732 db.execSQL( 1733 "CREATE INDEX IF NOT EXISTS path_index_lower ON files(_data COLLATE NOCASE);"); 1734 } 1735 if (fromVersion < 505) { 1736 // Starting with schema 505 we fill in the width/height/resolution columns for videos, 1737 // so force a rescan of videos to fill in the blanks 1738 db.execSQL("UPDATE files SET date_modified=0 WHERE " + FileColumns.MEDIA_TYPE + "=" 1739 + FileColumns.MEDIA_TYPE_VIDEO + ";"); 1740 } 1741 if (fromVersion < 506) { 1742 // sd card storage got moved to /storage/sdcard0 1743 // first delete everything that already got scanned in /storage before this 1744 // update step was added 1745 db.execSQL("DROP TRIGGER IF EXISTS files_cleanup"); 1746 db.execSQL("DELETE FROM files WHERE _data LIKE '/storage/%';"); 1747 db.execSQL("DELETE FROM album_art WHERE _data LIKE '/storage/%';"); 1748 db.execSQL("DELETE FROM thumbnails WHERE _data LIKE '/storage/%';"); 1749 db.execSQL("DELETE FROM videothumbnails WHERE _data LIKE '/storage/%';"); 1750 // then rename everything from /mnt/sdcard/ to /storage/sdcard0, 1751 // and from /mnt/external1 to /storage/sdcard1 1752 db.execSQL("UPDATE files SET " + 1753 "_data='/storage/sdcard0'||SUBSTR(_data,12) WHERE _data LIKE '/mnt/sdcard/%';"); 1754 db.execSQL("UPDATE files SET " + 1755 "_data='/storage/sdcard1'||SUBSTR(_data,15) WHERE _data LIKE '/mnt/external1/%';"); 1756 db.execSQL("UPDATE album_art SET " + 1757 "_data='/storage/sdcard0'||SUBSTR(_data,12) WHERE _data LIKE '/mnt/sdcard/%';"); 1758 db.execSQL("UPDATE album_art SET " + 1759 "_data='/storage/sdcard1'||SUBSTR(_data,15) WHERE _data LIKE '/mnt/external1/%';"); 1760 db.execSQL("UPDATE thumbnails SET " + 1761 "_data='/storage/sdcard0'||SUBSTR(_data,12) WHERE _data LIKE '/mnt/sdcard/%';"); 1762 db.execSQL("UPDATE thumbnails SET " + 1763 "_data='/storage/sdcard1'||SUBSTR(_data,15) WHERE _data LIKE '/mnt/external1/%';"); 1764 db.execSQL("UPDATE videothumbnails SET " + 1765 "_data='/storage/sdcard0'||SUBSTR(_data,12) WHERE _data LIKE '/mnt/sdcard/%';"); 1766 db.execSQL("UPDATE videothumbnails SET " + 1767 "_data='/storage/sdcard1'||SUBSTR(_data,15) WHERE _data LIKE '/mnt/external1/%';"); 1768 1769 if (!internal) { 1770 db.execSQL("CREATE TRIGGER IF NOT EXISTS files_cleanup DELETE ON files " + 1771 "BEGIN " + 1772 "SELECT _OBJECT_REMOVED(old._id);" + 1773 "END"); 1774 } 1775 } 1776 if (fromVersion < 507) { 1777 // we update _data in version 506, we need to update the bucket_id as well 1778 updateBucketNames(db); 1779 } 1780 if (fromVersion < 508 && !internal) { 1781 // ensure we don't get duplicate entries in the genre map 1782 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres_map_tmp (" + 1783 "_id INTEGER PRIMARY KEY," + 1784 "audio_id INTEGER NOT NULL," + 1785 "genre_id INTEGER NOT NULL," + 1786 "UNIQUE (audio_id,genre_id) ON CONFLICT IGNORE" + 1787 ");"); 1788 db.execSQL("INSERT INTO audio_genres_map_tmp (audio_id,genre_id)" + 1789 " SELECT DISTINCT audio_id,genre_id FROM audio_genres_map;"); 1790 db.execSQL("DROP TABLE audio_genres_map;"); 1791 db.execSQL("ALTER TABLE audio_genres_map_tmp RENAME TO audio_genres_map;"); 1792 } 1793 1794 if (fromVersion < 509) { 1795 db.execSQL("CREATE TABLE IF NOT EXISTS log (time DATETIME PRIMARY KEY, message TEXT);"); 1796 } 1797 1798 // Emulated external storage moved to user-specific paths 1799 if (fromVersion < 510 && Environment.isExternalStorageEmulated()) { 1800 // File.fixSlashes() removes any trailing slashes 1801 final String externalStorage = Environment.getExternalStorageDirectory().toString(); 1802 Log.d(TAG, "Adjusting external storage paths to: " + externalStorage); 1803 1804 final String[] tables = { 1805 TABLE_FILES, TABLE_ALBUM_ART, TABLE_THUMBNAILS, TABLE_VIDEO_THUMBNAILS }; 1806 for (String table : tables) { 1807 db.execSQL("UPDATE " + table + " SET " + "_data='" + externalStorage 1808 + "'||SUBSTR(_data,17) WHERE _data LIKE '/storage/sdcard0/%';"); 1809 } 1810 } 1811 if (fromVersion < 511) { 1812 // we update _data in version 510, we need to update the bucket_id as well 1813 updateBucketNames(db); 1814 } 1815 1816 // JB 4.2 went out with database version 511, starting next release with 600 1817 1818 if (fromVersion < 600) { 1819 // modify _data column to be unique and collate nocase. Because this drops the original 1820 // table and replaces it with a new one by the same name, we need to also recreate all 1821 // indices and triggers that refer to the files table. 1822 // Views don't need to be recreated. 1823 1824 db.execSQL("CREATE TABLE files2 (_id INTEGER PRIMARY KEY AUTOINCREMENT," + 1825 "_data TEXT UNIQUE" + 1826 // the internal filesystem is case-sensitive 1827 (internal ? "," : " COLLATE NOCASE,") + 1828 "_size INTEGER,format INTEGER,parent INTEGER,date_added INTEGER," + 1829 "date_modified INTEGER,mime_type TEXT,title TEXT,description TEXT," + 1830 "_display_name TEXT,picasa_id TEXT,orientation INTEGER,latitude DOUBLE," + 1831 "longitude DOUBLE,datetaken INTEGER,mini_thumb_magic INTEGER,bucket_id TEXT," + 1832 "bucket_display_name TEXT,isprivate INTEGER,title_key TEXT,artist_id INTEGER," + 1833 "album_id INTEGER,composer TEXT,track INTEGER,year INTEGER CHECK(year!=0)," + 1834 "is_ringtone INTEGER,is_music INTEGER,is_alarm INTEGER," + 1835 "is_notification INTEGER,is_podcast INTEGER,album_artist TEXT," + 1836 "duration INTEGER,bookmark INTEGER,artist TEXT,album TEXT,resolution TEXT," + 1837 "tags TEXT,category TEXT,language TEXT,mini_thumb_data TEXT,name TEXT," + 1838 "media_type INTEGER,old_id INTEGER,storage_id INTEGER,is_drm INTEGER," + 1839 "width INTEGER, height INTEGER);"); 1840 1841 // copy data from old table, squashing entries with duplicate _data 1842 db.execSQL("INSERT OR REPLACE INTO files2 SELECT * FROM files;"); 1843 db.execSQL("DROP TABLE files;"); 1844 db.execSQL("ALTER TABLE files2 RENAME TO files;"); 1845 1846 // recreate indices and triggers 1847 db.execSQL("CREATE INDEX album_id_idx ON files(album_id);"); 1848 db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id);"); 1849 db.execSQL("CREATE INDEX bucket_index on files(bucket_id,media_type," + 1850 "datetaken, _id);"); 1851 db.execSQL("CREATE INDEX bucket_name on files(bucket_id,media_type," + 1852 "bucket_display_name);"); 1853 db.execSQL("CREATE INDEX format_index ON files(format);"); 1854 db.execSQL("CREATE INDEX media_type_index ON files(media_type);"); 1855 db.execSQL("CREATE INDEX parent_index ON files(parent);"); 1856 db.execSQL("CREATE INDEX path_index ON files(_data);"); 1857 db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC);"); 1858 db.execSQL("CREATE INDEX title_idx ON files(title);"); 1859 db.execSQL("CREATE INDEX titlekey_index ON files(title_key);"); 1860 if (!internal) { 1861 db.execSQL("CREATE TRIGGER audio_playlists_cleanup DELETE ON files" + 1862 " WHEN old.media_type=4" + 1863 " BEGIN DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" + 1864 "SELECT _DELETE_FILE(old._data);END;"); 1865 db.execSQL("CREATE TRIGGER files_cleanup DELETE ON files" + 1866 " BEGIN SELECT _OBJECT_REMOVED(old._id);END;"); 1867 } 1868 } 1869 1870 if (fromVersion < 601) { 1871 // remove primary key constraint because column time is not necessarily unique 1872 db.execSQL("CREATE TABLE IF NOT EXISTS log_tmp (time DATETIME, message TEXT);"); 1873 db.execSQL("DELETE FROM log_tmp;"); 1874 db.execSQL("INSERT INTO log_tmp SELECT time, message FROM log order by rowid;"); 1875 db.execSQL("DROP TABLE log;"); 1876 db.execSQL("ALTER TABLE log_tmp RENAME TO log;"); 1877 } 1878 1879 if (fromVersion < 700) { 1880 // fix datetaken fields that were added with an incorrect timestamp 1881 // datetaken needs to be in milliseconds, so should generally be a few orders of 1882 // magnitude larger than date_modified. If it's within the same order of magnitude, it 1883 // is probably wrong. 1884 // (this could do the wrong thing if your picture was actually taken before ~3/21/1970) 1885 db.execSQL("UPDATE files set datetaken=date_modified*1000" 1886 + " WHERE date_modified IS NOT NULL" 1887 + " AND datetaken IS NOT NULL" 1888 + " AND datetaken<date_modified*5;"); 1889 } 1890 1891 if (fromVersion < 800) { 1892 // Delete albums and artists, then clear the modification time on songs, which 1893 // will cause the media scanner to rescan everything, rebuilding the artist and 1894 // album tables along the way, while preserving playlists. 1895 // We need this rescan because ICU also changed, and now generates different 1896 // collation keys 1897 db.execSQL("DELETE from albums"); 1898 db.execSQL("DELETE from artists"); 1899 db.execSQL("UPDATE files SET date_modified=0;"); 1900 } 1901 1902 sanityCheck(db, fromVersion); 1903 long elapsedSeconds = (SystemClock.currentTimeMicro() - startTime) / 1000000; 1904 logToDb(db, "Database upgraded from version " + fromVersion + " to " + toVersion 1905 + " in " + elapsedSeconds + " seconds"); 1906 } 1907 1908 /** 1909 * Write a persistent diagnostic message to the log table. 1910 */ logToDb(SQLiteDatabase db, String message)1911 static void logToDb(SQLiteDatabase db, String message) { 1912 db.execSQL("INSERT OR REPLACE" + 1913 " INTO log (time,message) VALUES (strftime('%Y-%m-%d %H:%M:%f','now'),?);", 1914 new String[] { message }); 1915 // delete all but the last 500 rows 1916 db.execSQL("DELETE FROM log WHERE rowid IN" + 1917 " (SELECT rowid FROM log ORDER BY rowid DESC LIMIT 500,-1);"); 1918 } 1919 1920 /** 1921 * Perform a simple sanity check on the database. Currently this tests 1922 * whether all the _data entries in audio_meta are unique 1923 */ sanityCheck(SQLiteDatabase db, int fromVersion)1924 private static void sanityCheck(SQLiteDatabase db, int fromVersion) { 1925 Cursor c1 = null; 1926 Cursor c2 = null; 1927 try { 1928 c1 = db.query("audio_meta", new String[] {"count(*)"}, 1929 null, null, null, null, null); 1930 c2 = db.query("audio_meta", new String[] {"count(distinct _data)"}, 1931 null, null, null, null, null); 1932 c1.moveToFirst(); 1933 c2.moveToFirst(); 1934 int num1 = c1.getInt(0); 1935 int num2 = c2.getInt(0); 1936 if (num1 != num2) { 1937 Log.e(TAG, "audio_meta._data column is not unique while upgrading" + 1938 " from schema " +fromVersion + " : " + num1 +"/" + num2); 1939 // Delete all audio_meta rows so they will be rebuilt by the media scanner 1940 db.execSQL("DELETE FROM audio_meta;"); 1941 } 1942 } finally { 1943 IoUtils.closeQuietly(c1); 1944 IoUtils.closeQuietly(c2); 1945 } 1946 } 1947 recreateAudioView(SQLiteDatabase db)1948 private static void recreateAudioView(SQLiteDatabase db) { 1949 // Provides a unified audio/artist/album info view. 1950 db.execSQL("DROP VIEW IF EXISTS audio"); 1951 db.execSQL("CREATE VIEW IF NOT EXISTS audio as SELECT * FROM audio_meta " + 1952 "LEFT OUTER JOIN artists ON audio_meta.artist_id=artists.artist_id " + 1953 "LEFT OUTER JOIN albums ON audio_meta.album_id=albums.album_id;"); 1954 } 1955 1956 /** 1957 * Update the bucket_id and bucket_display_name columns for images and videos 1958 * @param db 1959 * @param tableName 1960 */ updateBucketNames(SQLiteDatabase db)1961 private static void updateBucketNames(SQLiteDatabase db) { 1962 // Rebuild the bucket_display_name column using the natural case rather than lower case. 1963 db.beginTransaction(); 1964 try { 1965 String[] columns = {BaseColumns._ID, MediaColumns.DATA}; 1966 // update only images and videos 1967 Cursor cursor = db.query("files", columns, "media_type=1 OR media_type=3", 1968 null, null, null, null); 1969 try { 1970 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 1971 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 1972 String [] rowId = new String[1]; 1973 ContentValues values = new ContentValues(); 1974 while (cursor.moveToNext()) { 1975 String data = cursor.getString(dataColumnIndex); 1976 rowId[0] = cursor.getString(idColumnIndex); 1977 if (data != null) { 1978 values.clear(); 1979 computeBucketValues(data, values); 1980 db.update("files", values, "_id=?", rowId); 1981 } else { 1982 Log.w(TAG, "null data at id " + rowId); 1983 } 1984 } 1985 } finally { 1986 IoUtils.closeQuietly(cursor); 1987 } 1988 db.setTransactionSuccessful(); 1989 } finally { 1990 db.endTransaction(); 1991 } 1992 } 1993 1994 /** 1995 * Iterate through the rows of a table in a database, ensuring that the 1996 * display name column has a value. 1997 * @param db 1998 * @param tableName 1999 */ updateDisplayName(SQLiteDatabase db, String tableName)2000 private static void updateDisplayName(SQLiteDatabase db, String tableName) { 2001 // Fill in default values for null displayName values 2002 db.beginTransaction(); 2003 try { 2004 String[] columns = {BaseColumns._ID, MediaColumns.DATA, MediaColumns.DISPLAY_NAME}; 2005 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 2006 try { 2007 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 2008 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 2009 final int displayNameIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME); 2010 ContentValues values = new ContentValues(); 2011 while (cursor.moveToNext()) { 2012 String displayName = cursor.getString(displayNameIndex); 2013 if (displayName == null) { 2014 String data = cursor.getString(dataColumnIndex); 2015 values.clear(); 2016 computeDisplayName(data, values); 2017 int rowId = cursor.getInt(idColumnIndex); 2018 db.update(tableName, values, "_id=" + rowId, null); 2019 } 2020 } 2021 } finally { 2022 IoUtils.closeQuietly(cursor); 2023 } 2024 db.setTransactionSuccessful(); 2025 } finally { 2026 db.endTransaction(); 2027 } 2028 } 2029 2030 /** 2031 * @param data The input path 2032 * @param values the content values, where the bucked id name and bucket display name are updated. 2033 * 2034 */ computeBucketValues(String data, ContentValues values)2035 private static void computeBucketValues(String data, ContentValues values) { 2036 File parentFile = new File(data).getParentFile(); 2037 if (parentFile == null) { 2038 parentFile = new File("/"); 2039 } 2040 2041 // Lowercase the path for hashing. This avoids duplicate buckets if the 2042 // filepath case is changed externally. 2043 // Keep the original case for display. 2044 String path = parentFile.toString().toLowerCase(); 2045 String name = parentFile.getName(); 2046 2047 // Note: the BUCKET_ID and BUCKET_DISPLAY_NAME attributes are spelled the 2048 // same for both images and video. However, for backwards-compatibility reasons 2049 // there is no common base class. We use the ImageColumns version here 2050 values.put(ImageColumns.BUCKET_ID, path.hashCode()); 2051 values.put(ImageColumns.BUCKET_DISPLAY_NAME, name); 2052 } 2053 2054 /** 2055 * @param data The input path 2056 * @param values the content values, where the display name is updated. 2057 * 2058 */ computeDisplayName(String data, ContentValues values)2059 private static void computeDisplayName(String data, ContentValues values) { 2060 String s = (data == null ? "" : data.toString()); 2061 int idx = s.lastIndexOf('/'); 2062 if (idx >= 0) { 2063 s = s.substring(idx + 1); 2064 } 2065 values.put("_display_name", s); 2066 } 2067 2068 /** 2069 * Copy taken time from date_modified if we lost the original value (e.g. after factory reset) 2070 * This works for both video and image tables. 2071 * 2072 * @param values the content values, where taken time is updated. 2073 */ computeTakenTime(ContentValues values)2074 private static void computeTakenTime(ContentValues values) { 2075 if (! values.containsKey(Images.Media.DATE_TAKEN)) { 2076 // This only happens when MediaScanner finds an image file that doesn't have any useful 2077 // reference to get this value. (e.g. GPSTimeStamp) 2078 Long lastModified = values.getAsLong(MediaColumns.DATE_MODIFIED); 2079 if (lastModified != null) { 2080 values.put(Images.Media.DATE_TAKEN, lastModified * 1000); 2081 } 2082 } 2083 } 2084 2085 /** 2086 * This method blocks until thumbnail is ready. 2087 * 2088 * @param thumbUri 2089 * @return 2090 */ waitForThumbnailReady(Uri origUri)2091 private boolean waitForThumbnailReady(Uri origUri) { 2092 Cursor c = this.query(origUri, new String[] { ImageColumns._ID, ImageColumns.DATA, 2093 ImageColumns.MINI_THUMB_MAGIC}, null, null, null); 2094 boolean result = false; 2095 try { 2096 if (c != null && c.moveToFirst()) { 2097 long id = c.getLong(0); 2098 String path = c.getString(1); 2099 long magic = c.getLong(2); 2100 2101 MediaThumbRequest req = requestMediaThumbnail(path, origUri, 2102 MediaThumbRequest.PRIORITY_HIGH, magic); 2103 if (req != null) { 2104 synchronized (req) { 2105 try { 2106 while (req.mState == MediaThumbRequest.State.WAIT) { 2107 req.wait(); 2108 } 2109 } catch (InterruptedException e) { 2110 Log.w(TAG, e); 2111 } 2112 if (req.mState == MediaThumbRequest.State.DONE) { 2113 result = true; 2114 } 2115 } 2116 } 2117 } 2118 } finally { 2119 IoUtils.closeQuietly(c); 2120 } 2121 return result; 2122 } 2123 matchThumbRequest(MediaThumbRequest req, int pid, long id, long gid, boolean isVideo)2124 private boolean matchThumbRequest(MediaThumbRequest req, int pid, long id, long gid, 2125 boolean isVideo) { 2126 boolean cancelAllOrigId = (id == -1); 2127 boolean cancelAllGroupId = (gid == -1); 2128 return (req.mCallingPid == pid) && 2129 (cancelAllGroupId || req.mGroupId == gid) && 2130 (cancelAllOrigId || req.mOrigId == id) && 2131 (req.mIsVideo == isVideo); 2132 } 2133 queryThumbnail(SQLiteQueryBuilder qb, Uri uri, String table, String column, boolean hasThumbnailId)2134 private boolean queryThumbnail(SQLiteQueryBuilder qb, Uri uri, String table, 2135 String column, boolean hasThumbnailId) { 2136 qb.setTables(table); 2137 if (hasThumbnailId) { 2138 // For uri dispatched to this method, the 4th path segment is always 2139 // the thumbnail id. 2140 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 2141 // client already knows which thumbnail it wants, bypass it. 2142 return true; 2143 } 2144 String origId = uri.getQueryParameter("orig_id"); 2145 // We can't query ready_flag unless we know original id 2146 if (origId == null) { 2147 // this could be thumbnail query for other purpose, bypass it. 2148 return true; 2149 } 2150 2151 boolean needBlocking = "1".equals(uri.getQueryParameter("blocking")); 2152 boolean cancelRequest = "1".equals(uri.getQueryParameter("cancel")); 2153 Uri origUri = uri.buildUpon().encodedPath( 2154 uri.getPath().replaceFirst("thumbnails", "media")) 2155 .appendPath(origId).build(); 2156 2157 if (needBlocking && !waitForThumbnailReady(origUri)) { 2158 Log.w(TAG, "original media doesn't exist or it's canceled."); 2159 return false; 2160 } else if (cancelRequest) { 2161 String groupId = uri.getQueryParameter("group_id"); 2162 boolean isVideo = "video".equals(uri.getPathSegments().get(1)); 2163 int pid = Binder.getCallingPid(); 2164 long id = -1; 2165 long gid = -1; 2166 2167 try { 2168 id = Long.parseLong(origId); 2169 gid = Long.parseLong(groupId); 2170 } catch (NumberFormatException ex) { 2171 // invalid cancel request 2172 return false; 2173 } 2174 2175 synchronized (mMediaThumbQueue) { 2176 if (mCurrentThumbRequest != null && 2177 matchThumbRequest(mCurrentThumbRequest, pid, id, gid, isVideo)) { 2178 synchronized (mCurrentThumbRequest) { 2179 mCurrentThumbRequest.mState = MediaThumbRequest.State.CANCEL; 2180 mCurrentThumbRequest.notifyAll(); 2181 } 2182 } 2183 for (MediaThumbRequest mtq : mMediaThumbQueue) { 2184 if (matchThumbRequest(mtq, pid, id, gid, isVideo)) { 2185 synchronized (mtq) { 2186 mtq.mState = MediaThumbRequest.State.CANCEL; 2187 mtq.notifyAll(); 2188 } 2189 2190 mMediaThumbQueue.remove(mtq); 2191 } 2192 } 2193 } 2194 } 2195 2196 if (origId != null) { 2197 qb.appendWhere(column + " = " + origId); 2198 } 2199 return true; 2200 } 2201 2202 @Override canonicalize(Uri uri)2203 public Uri canonicalize(Uri uri) { 2204 int match = URI_MATCHER.match(uri); 2205 2206 // only support canonicalizing specific audio Uris 2207 if (match != AUDIO_MEDIA_ID) { 2208 return null; 2209 } 2210 Cursor c = query(uri, null, null, null, null); 2211 String title = null; 2212 Uri.Builder builder = null; 2213 2214 try { 2215 if (c == null || c.getCount() != 1 || !c.moveToNext()) { 2216 return null; 2217 } 2218 2219 // Construct a canonical Uri by tacking on some query parameters 2220 builder = uri.buildUpon(); 2221 builder.appendQueryParameter(CANONICAL, "1"); 2222 title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE)); 2223 } finally { 2224 IoUtils.closeQuietly(c); 2225 } 2226 if (TextUtils.isEmpty(title)) { 2227 return null; 2228 } 2229 builder.appendQueryParameter(MediaStore.Audio.Media.TITLE, title); 2230 Uri newUri = builder.build(); 2231 return newUri; 2232 } 2233 2234 @Override uncanonicalize(Uri uri)2235 public Uri uncanonicalize(Uri uri) { 2236 if (uri != null && "1".equals(uri.getQueryParameter(CANONICAL))) { 2237 int match = URI_MATCHER.match(uri); 2238 if (match != AUDIO_MEDIA_ID) { 2239 // this type of canonical Uri is not supported 2240 return null; 2241 } 2242 String titleFromUri = uri.getQueryParameter(MediaStore.Audio.Media.TITLE); 2243 if (titleFromUri == null) { 2244 // the required parameter is missing 2245 return null; 2246 } 2247 // clear the query parameters, we don't need them anymore 2248 uri = uri.buildUpon().clearQuery().build(); 2249 2250 Cursor c = query(uri, null, null, null, null); 2251 try { 2252 int titleIdx = c.getColumnIndex(MediaStore.Audio.Media.TITLE); 2253 if (c != null && c.getCount() == 1 && c.moveToNext() && 2254 titleFromUri.equals(c.getString(titleIdx))) { 2255 // the result matched perfectly 2256 return uri; 2257 } 2258 2259 IoUtils.closeQuietly(c); 2260 // do a lookup by title 2261 Uri newUri = MediaStore.Audio.Media.getContentUri(uri.getPathSegments().get(0)); 2262 2263 c = query(newUri, null, MediaStore.Audio.Media.TITLE + "=?", 2264 new String[] {titleFromUri}, null); 2265 if (c == null) { 2266 return null; 2267 } 2268 if (!c.moveToNext()) { 2269 return null; 2270 } 2271 // get the first matching entry and return a Uri for it 2272 long id = c.getLong(c.getColumnIndex(MediaStore.Audio.Media._ID)); 2273 return ContentUris.withAppendedId(newUri, id); 2274 } finally { 2275 IoUtils.closeQuietly(c); 2276 } 2277 } 2278 return uri; 2279 } 2280 safeUncanonicalize(Uri uri)2281 private Uri safeUncanonicalize(Uri uri) { 2282 Uri newUri = uncanonicalize(uri); 2283 if (newUri != null) { 2284 return newUri; 2285 } 2286 return uri; 2287 } 2288 2289 @SuppressWarnings("fallthrough") 2290 @Override query(Uri uri, String[] projectionIn, String selection, String[] selectionArgs, String sort)2291 public Cursor query(Uri uri, String[] projectionIn, String selection, 2292 String[] selectionArgs, String sort) { 2293 2294 uri = safeUncanonicalize(uri); 2295 2296 int table = URI_MATCHER.match(uri); 2297 List<String> prependArgs = new ArrayList<String>(); 2298 2299 // Log.v(TAG, "query: uri="+uri+", selection="+selection); 2300 // handle MEDIA_SCANNER before calling getDatabaseForUri() 2301 if (table == MEDIA_SCANNER) { 2302 if (mMediaScannerVolume == null) { 2303 return null; 2304 } else { 2305 // create a cursor to return volume currently being scanned by the media scanner 2306 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME}); 2307 c.addRow(new String[] {mMediaScannerVolume}); 2308 return c; 2309 } 2310 } 2311 2312 // Used temporarily (until we have unique media IDs) to get an identifier 2313 // for the current sd card, so that the music app doesn't have to use the 2314 // non-public getFatVolumeId method 2315 if (table == FS_ID) { 2316 MatrixCursor c = new MatrixCursor(new String[] {"fsid"}); 2317 c.addRow(new Integer[] {mVolumeId}); 2318 return c; 2319 } 2320 2321 if (table == VERSION) { 2322 MatrixCursor c = new MatrixCursor(new String[] {"version"}); 2323 c.addRow(new Integer[] {getDatabaseVersion(getContext())}); 2324 return c; 2325 } 2326 2327 String groupBy = null; 2328 DatabaseHelper helper = getDatabaseForUri(uri); 2329 if (helper == null) { 2330 return null; 2331 } 2332 helper.mNumQueries++; 2333 SQLiteDatabase db = helper.getReadableDatabase(); 2334 if (db == null) return null; 2335 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2336 String limit = uri.getQueryParameter("limit"); 2337 String filter = uri.getQueryParameter("filter"); 2338 String [] keywords = null; 2339 if (filter != null) { 2340 filter = Uri.decode(filter).trim(); 2341 if (!TextUtils.isEmpty(filter)) { 2342 String [] searchWords = filter.split(" "); 2343 keywords = new String[searchWords.length]; 2344 for (int i = 0; i < searchWords.length; i++) { 2345 String key = MediaStore.Audio.keyFor(searchWords[i]); 2346 key = key.replace("\\", "\\\\"); 2347 key = key.replace("%", "\\%"); 2348 key = key.replace("_", "\\_"); 2349 keywords[i] = key; 2350 } 2351 } 2352 } 2353 if (uri.getQueryParameter("distinct") != null) { 2354 qb.setDistinct(true); 2355 } 2356 2357 boolean hasThumbnailId = false; 2358 2359 switch (table) { 2360 case IMAGES_MEDIA: 2361 qb.setTables("images"); 2362 if (uri.getQueryParameter("distinct") != null) 2363 qb.setDistinct(true); 2364 2365 // set the project map so that data dir is prepended to _data. 2366 //qb.setProjectionMap(mImagesProjectionMap, true); 2367 break; 2368 2369 case IMAGES_MEDIA_ID: 2370 qb.setTables("images"); 2371 if (uri.getQueryParameter("distinct") != null) 2372 qb.setDistinct(true); 2373 2374 // set the project map so that data dir is prepended to _data. 2375 //qb.setProjectionMap(mImagesProjectionMap, true); 2376 qb.appendWhere("_id=?"); 2377 prependArgs.add(uri.getPathSegments().get(3)); 2378 break; 2379 2380 case IMAGES_THUMBNAILS_ID: 2381 hasThumbnailId = true; 2382 case IMAGES_THUMBNAILS: 2383 if (!queryThumbnail(qb, uri, "thumbnails", "image_id", hasThumbnailId)) { 2384 return null; 2385 } 2386 break; 2387 2388 case AUDIO_MEDIA: 2389 if (projectionIn != null && projectionIn.length == 1 && selectionArgs == null 2390 && (selection == null || selection.equalsIgnoreCase("is_music=1") 2391 || selection.equalsIgnoreCase("is_podcast=1") ) 2392 && projectionIn[0].equalsIgnoreCase("count(*)") 2393 && keywords != null) { 2394 //Log.i("@@@@", "taking fast path for counting songs"); 2395 qb.setTables("audio_meta"); 2396 } else { 2397 qb.setTables("audio"); 2398 for (int i = 0; keywords != null && i < keywords.length; i++) { 2399 if (i > 0) { 2400 qb.appendWhere(" AND "); 2401 } 2402 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2403 "||" + MediaStore.Audio.Media.ALBUM_KEY + 2404 "||" + MediaStore.Audio.Media.TITLE_KEY + " LIKE ? ESCAPE '\\'"); 2405 prependArgs.add("%" + keywords[i] + "%"); 2406 } 2407 } 2408 break; 2409 2410 case AUDIO_MEDIA_ID: 2411 qb.setTables("audio"); 2412 qb.appendWhere("_id=?"); 2413 prependArgs.add(uri.getPathSegments().get(3)); 2414 break; 2415 2416 case AUDIO_MEDIA_ID_GENRES: 2417 qb.setTables("audio_genres"); 2418 qb.appendWhere("_id IN (SELECT genre_id FROM " + 2419 "audio_genres_map WHERE audio_id=?)"); 2420 prependArgs.add(uri.getPathSegments().get(3)); 2421 break; 2422 2423 case AUDIO_MEDIA_ID_GENRES_ID: 2424 qb.setTables("audio_genres"); 2425 qb.appendWhere("_id=?"); 2426 prependArgs.add(uri.getPathSegments().get(5)); 2427 break; 2428 2429 case AUDIO_MEDIA_ID_PLAYLISTS: 2430 qb.setTables("audio_playlists"); 2431 qb.appendWhere("_id IN (SELECT playlist_id FROM " + 2432 "audio_playlists_map WHERE audio_id=?)"); 2433 prependArgs.add(uri.getPathSegments().get(3)); 2434 break; 2435 2436 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 2437 qb.setTables("audio_playlists"); 2438 qb.appendWhere("_id=?"); 2439 prependArgs.add(uri.getPathSegments().get(5)); 2440 break; 2441 2442 case AUDIO_GENRES: 2443 qb.setTables("audio_genres"); 2444 break; 2445 2446 case AUDIO_GENRES_ID: 2447 qb.setTables("audio_genres"); 2448 qb.appendWhere("_id=?"); 2449 prependArgs.add(uri.getPathSegments().get(3)); 2450 break; 2451 2452 case AUDIO_GENRES_ALL_MEMBERS: 2453 case AUDIO_GENRES_ID_MEMBERS: 2454 { 2455 // if simpleQuery is true, we can do a simpler query on just audio_genres_map 2456 // we can do this if we have no keywords and our projection includes just columns 2457 // from audio_genres_map 2458 boolean simpleQuery = (keywords == null && projectionIn != null 2459 && (selection == null || selection.equalsIgnoreCase("genre_id=?"))); 2460 if (projectionIn != null) { 2461 for (int i = 0; i < projectionIn.length; i++) { 2462 String p = projectionIn[i]; 2463 if (p.equals("_id")) { 2464 // note, this is different from playlist below, because 2465 // "_id" used to (wrongly) be the audio id in this query, not 2466 // the row id of the entry in the map, and we preserve this 2467 // behavior for backwards compatibility 2468 simpleQuery = false; 2469 } 2470 if (simpleQuery && !(p.equals("audio_id") || 2471 p.equals("genre_id"))) { 2472 simpleQuery = false; 2473 } 2474 } 2475 } 2476 if (simpleQuery) { 2477 qb.setTables("audio_genres_map_noid"); 2478 if (table == AUDIO_GENRES_ID_MEMBERS) { 2479 qb.appendWhere("genre_id=?"); 2480 prependArgs.add(uri.getPathSegments().get(3)); 2481 } 2482 } else { 2483 qb.setTables("audio_genres_map_noid, audio"); 2484 qb.appendWhere("audio._id = audio_id"); 2485 if (table == AUDIO_GENRES_ID_MEMBERS) { 2486 qb.appendWhere(" AND genre_id=?"); 2487 prependArgs.add(uri.getPathSegments().get(3)); 2488 } 2489 for (int i = 0; keywords != null && i < keywords.length; i++) { 2490 qb.appendWhere(" AND "); 2491 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2492 "||" + MediaStore.Audio.Media.ALBUM_KEY + 2493 "||" + MediaStore.Audio.Media.TITLE_KEY + 2494 " LIKE ? ESCAPE '\\'"); 2495 prependArgs.add("%" + keywords[i] + "%"); 2496 } 2497 } 2498 } 2499 break; 2500 2501 case AUDIO_PLAYLISTS: 2502 qb.setTables("audio_playlists"); 2503 break; 2504 2505 case AUDIO_PLAYLISTS_ID: 2506 qb.setTables("audio_playlists"); 2507 qb.appendWhere("_id=?"); 2508 prependArgs.add(uri.getPathSegments().get(3)); 2509 break; 2510 2511 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 2512 case AUDIO_PLAYLISTS_ID_MEMBERS: 2513 // if simpleQuery is true, we can do a simpler query on just audio_playlists_map 2514 // we can do this if we have no keywords and our projection includes just columns 2515 // from audio_playlists_map 2516 boolean simpleQuery = (keywords == null && projectionIn != null 2517 && (selection == null || selection.equalsIgnoreCase("playlist_id=?"))); 2518 if (projectionIn != null) { 2519 for (int i = 0; i < projectionIn.length; i++) { 2520 String p = projectionIn[i]; 2521 if (simpleQuery && !(p.equals("audio_id") || 2522 p.equals("playlist_id") || p.equals("play_order"))) { 2523 simpleQuery = false; 2524 } 2525 if (p.equals("_id")) { 2526 projectionIn[i] = "audio_playlists_map._id AS _id"; 2527 } 2528 } 2529 } 2530 if (simpleQuery) { 2531 qb.setTables("audio_playlists_map"); 2532 qb.appendWhere("playlist_id=?"); 2533 prependArgs.add(uri.getPathSegments().get(3)); 2534 } else { 2535 qb.setTables("audio_playlists_map, audio"); 2536 qb.appendWhere("audio._id = audio_id AND playlist_id=?"); 2537 prependArgs.add(uri.getPathSegments().get(3)); 2538 for (int i = 0; keywords != null && i < keywords.length; i++) { 2539 qb.appendWhere(" AND "); 2540 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2541 "||" + MediaStore.Audio.Media.ALBUM_KEY + 2542 "||" + MediaStore.Audio.Media.TITLE_KEY + 2543 " LIKE ? ESCAPE '\\'"); 2544 prependArgs.add("%" + keywords[i] + "%"); 2545 } 2546 } 2547 if (table == AUDIO_PLAYLISTS_ID_MEMBERS_ID) { 2548 qb.appendWhere(" AND audio_playlists_map._id=?"); 2549 prependArgs.add(uri.getPathSegments().get(5)); 2550 } 2551 break; 2552 2553 case VIDEO_MEDIA: 2554 qb.setTables("video"); 2555 break; 2556 case VIDEO_MEDIA_ID: 2557 qb.setTables("video"); 2558 qb.appendWhere("_id=?"); 2559 prependArgs.add(uri.getPathSegments().get(3)); 2560 break; 2561 2562 case VIDEO_THUMBNAILS_ID: 2563 hasThumbnailId = true; 2564 case VIDEO_THUMBNAILS: 2565 if (!queryThumbnail(qb, uri, "videothumbnails", "video_id", hasThumbnailId)) { 2566 return null; 2567 } 2568 break; 2569 2570 case AUDIO_ARTISTS: 2571 if (projectionIn != null && projectionIn.length == 1 && selectionArgs == null 2572 && (selection == null || selection.length() == 0) 2573 && projectionIn[0].equalsIgnoreCase("count(*)") 2574 && keywords != null) { 2575 //Log.i("@@@@", "taking fast path for counting artists"); 2576 qb.setTables("audio_meta"); 2577 projectionIn[0] = "count(distinct artist_id)"; 2578 qb.appendWhere("is_music=1"); 2579 } else { 2580 qb.setTables("artist_info"); 2581 for (int i = 0; keywords != null && i < keywords.length; i++) { 2582 if (i > 0) { 2583 qb.appendWhere(" AND "); 2584 } 2585 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2586 " LIKE ? ESCAPE '\\'"); 2587 prependArgs.add("%" + keywords[i] + "%"); 2588 } 2589 } 2590 break; 2591 2592 case AUDIO_ARTISTS_ID: 2593 qb.setTables("artist_info"); 2594 qb.appendWhere("_id=?"); 2595 prependArgs.add(uri.getPathSegments().get(3)); 2596 break; 2597 2598 case AUDIO_ARTISTS_ID_ALBUMS: 2599 String aid = uri.getPathSegments().get(3); 2600 qb.setTables("audio LEFT OUTER JOIN album_art ON" + 2601 " audio.album_id=album_art.album_id"); 2602 qb.appendWhere("is_music=1 AND audio.album_id IN (SELECT album_id FROM " + 2603 "artists_albums_map WHERE artist_id=?)"); 2604 prependArgs.add(aid); 2605 for (int i = 0; keywords != null && i < keywords.length; i++) { 2606 qb.appendWhere(" AND "); 2607 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2608 "||" + MediaStore.Audio.Media.ALBUM_KEY + 2609 " LIKE ? ESCAPE '\\'"); 2610 prependArgs.add("%" + keywords[i] + "%"); 2611 } 2612 groupBy = "audio.album_id"; 2613 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 2614 "count(CASE WHEN artist_id==" + aid + " THEN 'foo' ELSE NULL END) AS " + 2615 MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 2616 qb.setProjectionMap(sArtistAlbumsMap); 2617 break; 2618 2619 case AUDIO_ALBUMS: 2620 if (projectionIn != null && projectionIn.length == 1 && selectionArgs == null 2621 && (selection == null || selection.length() == 0) 2622 && projectionIn[0].equalsIgnoreCase("count(*)") 2623 && keywords != null) { 2624 //Log.i("@@@@", "taking fast path for counting albums"); 2625 qb.setTables("audio_meta"); 2626 projectionIn[0] = "count(distinct album_id)"; 2627 qb.appendWhere("is_music=1"); 2628 } else { 2629 qb.setTables("album_info"); 2630 for (int i = 0; keywords != null && i < keywords.length; i++) { 2631 if (i > 0) { 2632 qb.appendWhere(" AND "); 2633 } 2634 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2635 "||" + MediaStore.Audio.Media.ALBUM_KEY + 2636 " LIKE ? ESCAPE '\\'"); 2637 prependArgs.add("%" + keywords[i] + "%"); 2638 } 2639 } 2640 break; 2641 2642 case AUDIO_ALBUMS_ID: 2643 qb.setTables("album_info"); 2644 qb.appendWhere("_id=?"); 2645 prependArgs.add(uri.getPathSegments().get(3)); 2646 break; 2647 2648 case AUDIO_ALBUMART_ID: 2649 qb.setTables("album_art"); 2650 qb.appendWhere("album_id=?"); 2651 prependArgs.add(uri.getPathSegments().get(3)); 2652 break; 2653 2654 case AUDIO_SEARCH_LEGACY: 2655 Log.w(TAG, "Legacy media search Uri used. Please update your code."); 2656 // fall through 2657 case AUDIO_SEARCH_FANCY: 2658 case AUDIO_SEARCH_BASIC: 2659 return doAudioSearch(db, qb, uri, projectionIn, selection, 2660 combine(prependArgs, selectionArgs), sort, table, limit); 2661 2662 case FILES_ID: 2663 case MTP_OBJECTS_ID: 2664 qb.appendWhere("_id=?"); 2665 prependArgs.add(uri.getPathSegments().get(2)); 2666 // fall through 2667 case FILES: 2668 case MTP_OBJECTS: 2669 qb.setTables("files"); 2670 break; 2671 2672 case MTP_OBJECT_REFERENCES: 2673 int handle = Integer.parseInt(uri.getPathSegments().get(2)); 2674 return getObjectReferences(helper, db, handle); 2675 2676 default: 2677 throw new IllegalStateException("Unknown URL: " + uri.toString()); 2678 } 2679 2680 // Log.v(TAG, "query = "+ qb.buildQuery(projectionIn, selection, 2681 // combine(prependArgs, selectionArgs), groupBy, null, sort, limit)); 2682 Cursor c = qb.query(db, projectionIn, selection, 2683 combine(prependArgs, selectionArgs), groupBy, null, sort, limit); 2684 2685 if (c != null) { 2686 String nonotify = uri.getQueryParameter("nonotify"); 2687 if (nonotify == null || !nonotify.equals("1")) { 2688 c.setNotificationUri(getContext().getContentResolver(), uri); 2689 } 2690 } 2691 2692 return c; 2693 } 2694 combine(List<String> prepend, String[] userArgs)2695 private String[] combine(List<String> prepend, String[] userArgs) { 2696 int presize = prepend.size(); 2697 if (presize == 0) { 2698 return userArgs; 2699 } 2700 2701 int usersize = (userArgs != null) ? userArgs.length : 0; 2702 String [] combined = new String[presize + usersize]; 2703 for (int i = 0; i < presize; i++) { 2704 combined[i] = prepend.get(i); 2705 } 2706 for (int i = 0; i < usersize; i++) { 2707 combined[presize + i] = userArgs[i]; 2708 } 2709 return combined; 2710 } 2711 doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb, Uri uri, String[] projectionIn, String selection, String[] selectionArgs, String sort, int mode, String limit)2712 private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb, 2713 Uri uri, String[] projectionIn, String selection, 2714 String[] selectionArgs, String sort, int mode, 2715 String limit) { 2716 2717 String mSearchString = uri.getPath().endsWith("/") ? "" : uri.getLastPathSegment(); 2718 mSearchString = mSearchString.replaceAll(" ", " ").trim().toLowerCase(); 2719 2720 String [] searchWords = mSearchString.length() > 0 ? 2721 mSearchString.split(" ") : new String[0]; 2722 String [] wildcardWords = new String[searchWords.length]; 2723 int len = searchWords.length; 2724 for (int i = 0; i < len; i++) { 2725 // Because we match on individual words here, we need to remove words 2726 // like 'a' and 'the' that aren't part of the keys. 2727 String key = MediaStore.Audio.keyFor(searchWords[i]); 2728 key = key.replace("\\", "\\\\"); 2729 key = key.replace("%", "\\%"); 2730 key = key.replace("_", "\\_"); 2731 wildcardWords[i] = 2732 (searchWords[i].equals("a") || searchWords[i].equals("an") || 2733 searchWords[i].equals("the")) ? "%" : "%" + key + "%"; 2734 } 2735 2736 String where = ""; 2737 for (int i = 0; i < searchWords.length; i++) { 2738 if (i == 0) { 2739 where = "match LIKE ? ESCAPE '\\'"; 2740 } else { 2741 where += " AND match LIKE ? ESCAPE '\\'"; 2742 } 2743 } 2744 2745 qb.setTables("search"); 2746 String [] cols; 2747 if (mode == AUDIO_SEARCH_FANCY) { 2748 cols = mSearchColsFancy; 2749 } else if (mode == AUDIO_SEARCH_BASIC) { 2750 cols = mSearchColsBasic; 2751 } else { 2752 cols = mSearchColsLegacy; 2753 } 2754 return qb.query(db, cols, where, wildcardWords, null, null, null, limit); 2755 } 2756 2757 @Override getType(Uri url)2758 public String getType(Uri url) 2759 { 2760 switch (URI_MATCHER.match(url)) { 2761 case IMAGES_MEDIA_ID: 2762 case AUDIO_MEDIA_ID: 2763 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 2764 case VIDEO_MEDIA_ID: 2765 case FILES_ID: 2766 Cursor c = null; 2767 try { 2768 c = query(url, MIME_TYPE_PROJECTION, null, null, null); 2769 if (c != null && c.getCount() == 1) { 2770 c.moveToFirst(); 2771 String mimeType = c.getString(1); 2772 c.deactivate(); 2773 return mimeType; 2774 } 2775 } finally { 2776 IoUtils.closeQuietly(c); 2777 } 2778 break; 2779 2780 case IMAGES_MEDIA: 2781 case IMAGES_THUMBNAILS: 2782 return Images.Media.CONTENT_TYPE; 2783 case AUDIO_ALBUMART_ID: 2784 case IMAGES_THUMBNAILS_ID: 2785 return "image/jpeg"; 2786 2787 case AUDIO_MEDIA: 2788 case AUDIO_GENRES_ID_MEMBERS: 2789 case AUDIO_PLAYLISTS_ID_MEMBERS: 2790 return Audio.Media.CONTENT_TYPE; 2791 2792 case AUDIO_GENRES: 2793 case AUDIO_MEDIA_ID_GENRES: 2794 return Audio.Genres.CONTENT_TYPE; 2795 case AUDIO_GENRES_ID: 2796 case AUDIO_MEDIA_ID_GENRES_ID: 2797 return Audio.Genres.ENTRY_CONTENT_TYPE; 2798 case AUDIO_PLAYLISTS: 2799 case AUDIO_MEDIA_ID_PLAYLISTS: 2800 return Audio.Playlists.CONTENT_TYPE; 2801 case AUDIO_PLAYLISTS_ID: 2802 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 2803 return Audio.Playlists.ENTRY_CONTENT_TYPE; 2804 2805 case VIDEO_MEDIA: 2806 return Video.Media.CONTENT_TYPE; 2807 } 2808 throw new IllegalStateException("Unknown URL : " + url); 2809 } 2810 2811 /** 2812 * Ensures there is a file in the _data column of values, if one isn't 2813 * present a new filename is generated. The file itself is not created. 2814 * 2815 * @param initialValues the values passed to insert by the caller 2816 * @return the new values 2817 */ ensureFile(boolean internal, ContentValues initialValues, String preferredExtension, String directoryName)2818 private ContentValues ensureFile(boolean internal, ContentValues initialValues, 2819 String preferredExtension, String directoryName) { 2820 ContentValues values; 2821 String file = initialValues.getAsString(MediaStore.MediaColumns.DATA); 2822 if (TextUtils.isEmpty(file)) { 2823 file = generateFileName(internal, preferredExtension, directoryName); 2824 values = new ContentValues(initialValues); 2825 values.put(MediaStore.MediaColumns.DATA, file); 2826 } else { 2827 values = initialValues; 2828 } 2829 2830 // we used to create the file here, but now defer this until openFile() is called 2831 return values; 2832 } 2833 sendObjectAdded(long objectHandle)2834 private void sendObjectAdded(long objectHandle) { 2835 synchronized (mMtpServiceConnection) { 2836 if (mMtpService != null) { 2837 try { 2838 mMtpService.sendObjectAdded((int)objectHandle); 2839 } catch (RemoteException e) { 2840 Log.e(TAG, "RemoteException in sendObjectAdded", e); 2841 mMtpService = null; 2842 } 2843 } 2844 } 2845 } 2846 sendObjectRemoved(long objectHandle)2847 private void sendObjectRemoved(long objectHandle) { 2848 synchronized (mMtpServiceConnection) { 2849 if (mMtpService != null) { 2850 try { 2851 mMtpService.sendObjectRemoved((int)objectHandle); 2852 } catch (RemoteException e) { 2853 Log.e(TAG, "RemoteException in sendObjectRemoved", e); 2854 mMtpService = null; 2855 } 2856 } 2857 } 2858 } 2859 2860 @Override bulkInsert(Uri uri, ContentValues values[])2861 public int bulkInsert(Uri uri, ContentValues values[]) { 2862 int match = URI_MATCHER.match(uri); 2863 if (match == VOLUMES) { 2864 return super.bulkInsert(uri, values); 2865 } 2866 DatabaseHelper helper = getDatabaseForUri(uri); 2867 if (helper == null) { 2868 throw new UnsupportedOperationException( 2869 "Unknown URI: " + uri); 2870 } 2871 SQLiteDatabase db = helper.getWritableDatabase(); 2872 if (db == null) { 2873 throw new IllegalStateException("Couldn't open database for " + uri); 2874 } 2875 2876 if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) { 2877 return playlistBulkInsert(db, uri, values); 2878 } else if (match == MTP_OBJECT_REFERENCES) { 2879 int handle = Integer.parseInt(uri.getPathSegments().get(2)); 2880 return setObjectReferences(helper, db, handle, values); 2881 } 2882 2883 2884 db.beginTransaction(); 2885 ArrayList<Long> notifyRowIds = new ArrayList<Long>(); 2886 int numInserted = 0; 2887 try { 2888 int len = values.length; 2889 for (int i = 0; i < len; i++) { 2890 if (values[i] != null) { 2891 insertInternal(uri, match, values[i], notifyRowIds); 2892 } 2893 } 2894 numInserted = len; 2895 db.setTransactionSuccessful(); 2896 } finally { 2897 db.endTransaction(); 2898 } 2899 2900 // Notify MTP (outside of successful transaction) 2901 if (uri != null) { 2902 if (uri.toString().startsWith("content://media/external/")) { 2903 notifyMtp(notifyRowIds); 2904 } 2905 } 2906 2907 getContext().getContentResolver().notifyChange(uri, null); 2908 return numInserted; 2909 } 2910 2911 @Override insert(Uri uri, ContentValues initialValues)2912 public Uri insert(Uri uri, ContentValues initialValues) { 2913 int match = URI_MATCHER.match(uri); 2914 2915 ArrayList<Long> notifyRowIds = new ArrayList<Long>(); 2916 Uri newUri = insertInternal(uri, match, initialValues, notifyRowIds); 2917 if (uri != null) { 2918 if (uri.toString().startsWith("content://media/external/")) { 2919 notifyMtp(notifyRowIds); 2920 } 2921 } 2922 2923 // do not signal notification for MTP objects. 2924 // we will signal instead after file transfer is successful. 2925 if (newUri != null && match != MTP_OBJECTS) { 2926 // Report a general change to the media provider. 2927 // We only report this to observers that are not looking at 2928 // this specific URI and its descendants, because they will 2929 // still see the following more-specific URI and thus get 2930 // redundant info (and not be able to know if there was just 2931 // the specific URI change or also some general change in the 2932 // parent URI). 2933 getContext().getContentResolver().notifyChange(uri, null, match != MEDIA_SCANNER 2934 ? ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS : 0); 2935 // Also report the specific URIs that changed. 2936 if (match != MEDIA_SCANNER) { 2937 getContext().getContentResolver().notifyChange(newUri, null, 0); 2938 } 2939 } 2940 return newUri; 2941 } 2942 notifyMtp(ArrayList<Long> rowIds)2943 private void notifyMtp(ArrayList<Long> rowIds) { 2944 int size = rowIds.size(); 2945 for (int i = 0; i < size; i++) { 2946 sendObjectAdded(rowIds.get(i).longValue()); 2947 } 2948 } 2949 playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[])2950 private int playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[]) { 2951 DatabaseUtils.InsertHelper helper = 2952 new DatabaseUtils.InsertHelper(db, "audio_playlists_map"); 2953 int audioidcolidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID); 2954 int playlistididx = helper.getColumnIndex(Audio.Playlists.Members.PLAYLIST_ID); 2955 int playorderidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 2956 long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 2957 2958 db.beginTransaction(); 2959 int numInserted = 0; 2960 try { 2961 int len = values.length; 2962 for (int i = 0; i < len; i++) { 2963 helper.prepareForInsert(); 2964 // getting the raw Object and converting it long ourselves saves 2965 // an allocation (the alternative is ContentValues.getAsLong, which 2966 // returns a Long object) 2967 long audioid = ((Number) values[i].get( 2968 MediaStore.Audio.Playlists.Members.AUDIO_ID)).longValue(); 2969 helper.bind(audioidcolidx, audioid); 2970 helper.bind(playlistididx, playlistId); 2971 // convert to int ourselves to save an allocation. 2972 int playorder = ((Number) values[i].get( 2973 MediaStore.Audio.Playlists.Members.PLAY_ORDER)).intValue(); 2974 helper.bind(playorderidx, playorder); 2975 helper.execute(); 2976 } 2977 numInserted = len; 2978 db.setTransactionSuccessful(); 2979 } finally { 2980 db.endTransaction(); 2981 helper.close(); 2982 } 2983 getContext().getContentResolver().notifyChange(uri, null); 2984 return numInserted; 2985 } 2986 insertDirectory(DatabaseHelper helper, SQLiteDatabase db, String path)2987 private long insertDirectory(DatabaseHelper helper, SQLiteDatabase db, String path) { 2988 if (LOCAL_LOGV) Log.v(TAG, "inserting directory " + path); 2989 ContentValues values = new ContentValues(); 2990 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 2991 values.put(FileColumns.DATA, path); 2992 values.put(FileColumns.PARENT, getParent(helper, db, path)); 2993 values.put(FileColumns.STORAGE_ID, getStorageId(path)); 2994 File file = new File(path); 2995 if (file.exists()) { 2996 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 2997 } 2998 helper.mNumInserts++; 2999 long rowId = db.insert("files", FileColumns.DATE_MODIFIED, values); 3000 sendObjectAdded(rowId); 3001 return rowId; 3002 } 3003 getParent(DatabaseHelper helper, SQLiteDatabase db, String path)3004 private long getParent(DatabaseHelper helper, SQLiteDatabase db, String path) { 3005 int lastSlash = path.lastIndexOf('/'); 3006 if (lastSlash > 0) { 3007 String parentPath = path.substring(0, lastSlash); 3008 for (int i = 0; i < mExternalStoragePaths.length; i++) { 3009 if (parentPath.equals(mExternalStoragePaths[i])) { 3010 return 0; 3011 } 3012 } 3013 Long cid = mDirectoryCache.get(parentPath); 3014 if (cid != null) { 3015 if (LOCAL_LOGV) Log.v(TAG, "Returning cached entry for " + parentPath); 3016 return cid; 3017 } 3018 3019 String selection = MediaStore.MediaColumns.DATA + "=?"; 3020 String [] selargs = { parentPath }; 3021 helper.mNumQueries++; 3022 Cursor c = db.query("files", sIdOnlyColumn, selection, selargs, null, null, null); 3023 try { 3024 long id; 3025 if (c == null || c.getCount() == 0) { 3026 // parent isn't in the database - so add it 3027 id = insertDirectory(helper, db, parentPath); 3028 if (LOCAL_LOGV) Log.v(TAG, "Inserted " + parentPath); 3029 } else { 3030 if (c.getCount() > 1) { 3031 Log.e(TAG, "more than one match for " + parentPath); 3032 } 3033 c.moveToFirst(); 3034 id = c.getLong(0); 3035 if (LOCAL_LOGV) Log.v(TAG, "Queried " + parentPath); 3036 } 3037 mDirectoryCache.put(parentPath, id); 3038 return id; 3039 } finally { 3040 IoUtils.closeQuietly(c); 3041 } 3042 } else { 3043 return 0; 3044 } 3045 } 3046 getStorageId(String path)3047 private int getStorageId(String path) { 3048 final StorageManager storage = getContext().getSystemService(StorageManager.class); 3049 final StorageVolume vol = storage.getStorageVolume(new File(path)); 3050 if (vol != null) { 3051 return vol.getStorageId(); 3052 } else { 3053 Log.w(TAG, "Missing volume for " + path + "; assuming invalid"); 3054 return StorageVolume.STORAGE_ID_INVALID; 3055 } 3056 } 3057 insertFile(DatabaseHelper helper, Uri uri, ContentValues initialValues, int mediaType, boolean notify, ArrayList<Long> notifyRowIds)3058 private long insertFile(DatabaseHelper helper, Uri uri, ContentValues initialValues, int mediaType, 3059 boolean notify, ArrayList<Long> notifyRowIds) { 3060 SQLiteDatabase db = helper.getWritableDatabase(); 3061 ContentValues values = null; 3062 3063 switch (mediaType) { 3064 case FileColumns.MEDIA_TYPE_IMAGE: { 3065 values = ensureFile(helper.mInternal, initialValues, ".jpg", 3066 Environment.DIRECTORY_PICTURES); 3067 3068 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 3069 String data = values.getAsString(MediaColumns.DATA); 3070 if (! values.containsKey(MediaColumns.DISPLAY_NAME)) { 3071 computeDisplayName(data, values); 3072 } 3073 computeTakenTime(values); 3074 break; 3075 } 3076 3077 case FileColumns.MEDIA_TYPE_AUDIO: { 3078 // SQLite Views are read-only, so we need to deconstruct this 3079 // insert and do inserts into the underlying tables. 3080 // If doing this here turns out to be a performance bottleneck, 3081 // consider moving this to native code and using triggers on 3082 // the view. 3083 values = new ContentValues(initialValues); 3084 3085 String albumartist = values.getAsString(MediaStore.Audio.Media.ALBUM_ARTIST); 3086 String compilation = values.getAsString(MediaStore.Audio.Media.COMPILATION); 3087 values.remove(MediaStore.Audio.Media.COMPILATION); 3088 3089 // Insert the artist into the artist table and remove it from 3090 // the input values 3091 Object so = values.get("artist"); 3092 String s = (so == null ? "" : so.toString()); 3093 values.remove("artist"); 3094 long artistRowId; 3095 HashMap<String, Long> artistCache = helper.mArtistCache; 3096 String path = values.getAsString(MediaStore.MediaColumns.DATA); 3097 synchronized(artistCache) { 3098 Long temp = artistCache.get(s); 3099 if (temp == null) { 3100 artistRowId = getKeyIdForName(helper, db, 3101 "artists", "artist_key", "artist", 3102 s, s, path, 0, null, artistCache, uri); 3103 } else { 3104 artistRowId = temp.longValue(); 3105 } 3106 } 3107 String artist = s; 3108 3109 // Do the same for the album field 3110 so = values.get("album"); 3111 s = (so == null ? "" : so.toString()); 3112 values.remove("album"); 3113 long albumRowId; 3114 HashMap<String, Long> albumCache = helper.mAlbumCache; 3115 synchronized(albumCache) { 3116 int albumhash = 0; 3117 if (albumartist != null) { 3118 albumhash = albumartist.hashCode(); 3119 } else if (compilation != null && compilation.equals("1")) { 3120 // nothing to do, hash already set 3121 } else { 3122 albumhash = path.substring(0, path.lastIndexOf('/')).hashCode(); 3123 } 3124 String cacheName = s + albumhash; 3125 Long temp = albumCache.get(cacheName); 3126 if (temp == null) { 3127 albumRowId = getKeyIdForName(helper, db, 3128 "albums", "album_key", "album", 3129 s, cacheName, path, albumhash, artist, albumCache, uri); 3130 } else { 3131 albumRowId = temp; 3132 } 3133 } 3134 3135 values.put("artist_id", Integer.toString((int)artistRowId)); 3136 values.put("album_id", Integer.toString((int)albumRowId)); 3137 so = values.getAsString("title"); 3138 s = (so == null ? "" : so.toString()); 3139 values.put("title_key", MediaStore.Audio.keyFor(s)); 3140 // do a final trim of the title, in case it started with the special 3141 // "sort first" character (ascii \001) 3142 values.remove("title"); 3143 values.put("title", s.trim()); 3144 3145 computeDisplayName(values.getAsString(MediaStore.MediaColumns.DATA), values); 3146 break; 3147 } 3148 3149 case FileColumns.MEDIA_TYPE_VIDEO: { 3150 values = ensureFile(helper.mInternal, initialValues, ".3gp", "video"); 3151 String data = values.getAsString(MediaStore.MediaColumns.DATA); 3152 computeDisplayName(data, values); 3153 computeTakenTime(values); 3154 break; 3155 } 3156 } 3157 3158 if (values == null) { 3159 values = new ContentValues(initialValues); 3160 } 3161 // compute bucket_id and bucket_display_name for all files 3162 String path = values.getAsString(MediaStore.MediaColumns.DATA); 3163 if (path != null) { 3164 computeBucketValues(path, values); 3165 } 3166 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 3167 3168 long rowId = 0; 3169 Integer i = values.getAsInteger( 3170 MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID); 3171 if (i != null) { 3172 rowId = i.intValue(); 3173 values = new ContentValues(values); 3174 values.remove(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID); 3175 } 3176 3177 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 3178 if (title == null && path != null) { 3179 title = MediaFile.getFileTitle(path); 3180 } 3181 values.put(FileColumns.TITLE, title); 3182 3183 String mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE); 3184 Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 3185 int format = (formatObject == null ? 0 : formatObject.intValue()); 3186 if (format == 0) { 3187 if (TextUtils.isEmpty(path)) { 3188 // special case device created playlists 3189 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 3190 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST); 3191 // create a file path for the benefit of MTP 3192 path = mExternalStoragePaths[0] 3193 + "/Playlists/" + values.getAsString(Audio.Playlists.NAME); 3194 values.put(MediaStore.MediaColumns.DATA, path); 3195 values.put(FileColumns.PARENT, getParent(helper, db, path)); 3196 } else { 3197 Log.e(TAG, "path is empty in insertFile()"); 3198 } 3199 } else { 3200 format = MediaFile.getFormatCode(path, mimeType); 3201 } 3202 } 3203 if (format != 0) { 3204 values.put(FileColumns.FORMAT, format); 3205 if (mimeType == null) { 3206 mimeType = MediaFile.getMimeTypeForFormatCode(format); 3207 } 3208 } 3209 3210 if (mimeType == null && path != null) { 3211 mimeType = MediaFile.getMimeTypeForFile(path); 3212 } 3213 if (mimeType != null) { 3214 values.put(FileColumns.MIME_TYPE, mimeType); 3215 3216 if (mediaType == FileColumns.MEDIA_TYPE_NONE && !MediaScanner.isNoMediaPath(path)) { 3217 int fileType = MediaFile.getFileTypeForMimeType(mimeType); 3218 if (MediaFile.isAudioFileType(fileType)) { 3219 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 3220 } else if (MediaFile.isVideoFileType(fileType)) { 3221 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 3222 } else if (MediaFile.isImageFileType(fileType)) { 3223 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 3224 } else if (MediaFile.isPlayListFileType(fileType)) { 3225 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 3226 } 3227 } 3228 } 3229 values.put(FileColumns.MEDIA_TYPE, mediaType); 3230 3231 if (rowId == 0) { 3232 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 3233 String name = values.getAsString(Audio.Playlists.NAME); 3234 if (name == null && path == null) { 3235 // MediaScanner will compute the name from the path if we have one 3236 throw new IllegalArgumentException( 3237 "no name was provided when inserting abstract playlist"); 3238 } 3239 } else { 3240 if (path == null) { 3241 // path might be null for playlists created on the device 3242 // or transfered via MTP 3243 throw new IllegalArgumentException( 3244 "no path was provided when inserting new file"); 3245 } 3246 } 3247 3248 // make sure modification date and size are set 3249 if (path != null) { 3250 File file = new File(path); 3251 if (file.exists()) { 3252 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 3253 if (!values.containsKey(FileColumns.SIZE)) { 3254 values.put(FileColumns.SIZE, file.length()); 3255 } 3256 // make sure date taken time is set 3257 if (mediaType == FileColumns.MEDIA_TYPE_IMAGE 3258 || mediaType == FileColumns.MEDIA_TYPE_VIDEO) { 3259 computeTakenTime(values); 3260 } 3261 } 3262 } 3263 3264 Long parent = values.getAsLong(FileColumns.PARENT); 3265 if (parent == null) { 3266 if (path != null) { 3267 long parentId = getParent(helper, db, path); 3268 values.put(FileColumns.PARENT, parentId); 3269 } 3270 } 3271 Integer storage = values.getAsInteger(FileColumns.STORAGE_ID); 3272 if (storage == null) { 3273 int storageId = getStorageId(path); 3274 values.put(FileColumns.STORAGE_ID, storageId); 3275 } 3276 3277 helper.mNumInserts++; 3278 rowId = db.insert("files", FileColumns.DATE_MODIFIED, values); 3279 if (LOCAL_LOGV) Log.v(TAG, "insertFile: values=" + values + " returned: " + rowId); 3280 3281 if (rowId != -1 && notify) { 3282 notifyRowIds.add(rowId); 3283 } 3284 } else { 3285 helper.mNumUpdates++; 3286 db.update("files", values, FileColumns._ID + "=?", 3287 new String[] { Long.toString(rowId) }); 3288 } 3289 if (format == MtpConstants.FORMAT_ASSOCIATION) { 3290 mDirectoryCache.put(path, rowId); 3291 } 3292 3293 return rowId; 3294 } 3295 getObjectReferences(DatabaseHelper helper, SQLiteDatabase db, int handle)3296 private Cursor getObjectReferences(DatabaseHelper helper, SQLiteDatabase db, int handle) { 3297 helper.mNumQueries++; 3298 Cursor c = db.query("files", sMediaTableColumns, "_id=?", 3299 new String[] { Integer.toString(handle) }, 3300 null, null, null); 3301 try { 3302 if (c != null && c.moveToNext()) { 3303 long playlistId = c.getLong(0); 3304 int mediaType = c.getInt(1); 3305 if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) { 3306 // we only support object references for playlist objects 3307 return null; 3308 } 3309 helper.mNumQueries++; 3310 return db.rawQuery(OBJECT_REFERENCES_QUERY, 3311 new String[] { Long.toString(playlistId) } ); 3312 } 3313 } finally { 3314 IoUtils.closeQuietly(c); 3315 } 3316 return null; 3317 } 3318 setObjectReferences(DatabaseHelper helper, SQLiteDatabase db, int handle, ContentValues values[])3319 private int setObjectReferences(DatabaseHelper helper, SQLiteDatabase db, 3320 int handle, ContentValues values[]) { 3321 // first look up the media table and media ID for the object 3322 long playlistId = 0; 3323 helper.mNumQueries++; 3324 Cursor c = db.query("files", sMediaTableColumns, "_id=?", 3325 new String[] { Integer.toString(handle) }, 3326 null, null, null); 3327 try { 3328 if (c != null && c.moveToNext()) { 3329 int mediaType = c.getInt(1); 3330 if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) { 3331 // we only support object references for playlist objects 3332 return 0; 3333 } 3334 playlistId = c.getLong(0); 3335 } 3336 } finally { 3337 IoUtils.closeQuietly(c); 3338 } 3339 if (playlistId == 0) { 3340 return 0; 3341 } 3342 3343 // next delete any existing entries 3344 helper.mNumDeletes++; 3345 db.delete("audio_playlists_map", "playlist_id=?", 3346 new String[] { Long.toString(playlistId) }); 3347 3348 // finally add the new entries 3349 int count = values.length; 3350 int added = 0; 3351 ContentValues[] valuesList = new ContentValues[count]; 3352 for (int i = 0; i < count; i++) { 3353 // convert object ID to audio ID 3354 long audioId = 0; 3355 long objectId = values[i].getAsLong(MediaStore.MediaColumns._ID); 3356 helper.mNumQueries++; 3357 c = db.query("files", sMediaTableColumns, "_id=?", 3358 new String[] { Long.toString(objectId) }, 3359 null, null, null); 3360 try { 3361 if (c != null && c.moveToNext()) { 3362 int mediaType = c.getInt(1); 3363 if (mediaType != FileColumns.MEDIA_TYPE_AUDIO) { 3364 // we only allow audio files in playlists, so skip 3365 continue; 3366 } 3367 audioId = c.getLong(0); 3368 } 3369 } finally { 3370 IoUtils.closeQuietly(c); 3371 } 3372 if (audioId != 0) { 3373 ContentValues v = new ContentValues(); 3374 v.put(MediaStore.Audio.Playlists.Members.PLAYLIST_ID, playlistId); 3375 v.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId); 3376 v.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, added); 3377 valuesList[added++] = v; 3378 } 3379 } 3380 if (added < count) { 3381 // we weren't able to find everything on the list, so lets resize the array 3382 // and pass what we have. 3383 ContentValues[] newValues = new ContentValues[added]; 3384 System.arraycopy(valuesList, 0, newValues, 0, added); 3385 valuesList = newValues; 3386 } 3387 return playlistBulkInsert(db, 3388 Audio.Playlists.Members.getContentUri(EXTERNAL_VOLUME, playlistId), 3389 valuesList); 3390 } 3391 3392 private static final String[] GENRE_LOOKUP_PROJECTION = new String[] { 3393 Audio.Genres._ID, // 0 3394 Audio.Genres.NAME, // 1 3395 }; 3396 updateGenre(long rowId, String genre)3397 private void updateGenre(long rowId, String genre) { 3398 Uri uri = null; 3399 Cursor cursor = null; 3400 Uri genresUri = MediaStore.Audio.Genres.getContentUri("external"); 3401 try { 3402 // see if the genre already exists 3403 cursor = query(genresUri, GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", 3404 new String[] { genre }, null); 3405 if (cursor == null || cursor.getCount() == 0) { 3406 // genre does not exist, so create the genre in the genre table 3407 ContentValues values = new ContentValues(); 3408 values.put(MediaStore.Audio.Genres.NAME, genre); 3409 uri = insert(genresUri, values); 3410 } else { 3411 // genre already exists, so compute its Uri 3412 cursor.moveToNext(); 3413 uri = ContentUris.withAppendedId(genresUri, cursor.getLong(0)); 3414 } 3415 if (uri != null) { 3416 uri = Uri.withAppendedPath(uri, MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY); 3417 } 3418 } finally { 3419 IoUtils.closeQuietly(cursor); 3420 } 3421 3422 if (uri != null) { 3423 // add entry to audio_genre_map 3424 ContentValues values = new ContentValues(); 3425 values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); 3426 insert(uri, values); 3427 } 3428 } 3429 insertInternal(Uri uri, int match, ContentValues initialValues, ArrayList<Long> notifyRowIds)3430 private Uri insertInternal(Uri uri, int match, ContentValues initialValues, 3431 ArrayList<Long> notifyRowIds) { 3432 final String volumeName = getVolumeName(uri); 3433 3434 long rowId; 3435 3436 if (LOCAL_LOGV) Log.v(TAG, "insertInternal: "+uri+", initValues="+initialValues); 3437 // handle MEDIA_SCANNER before calling getDatabaseForUri() 3438 if (match == MEDIA_SCANNER) { 3439 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 3440 DatabaseHelper database = getDatabaseForUri( 3441 Uri.parse("content://media/" + mMediaScannerVolume + "/audio")); 3442 if (database == null) { 3443 Log.w(TAG, "no database for scanned volume " + mMediaScannerVolume); 3444 } else { 3445 database.mScanStartTime = SystemClock.currentTimeMicro(); 3446 } 3447 return MediaStore.getMediaScannerUri(); 3448 } 3449 3450 String genre = null; 3451 String path = null; 3452 if (initialValues != null) { 3453 genre = initialValues.getAsString(Audio.AudioColumns.GENRE); 3454 initialValues.remove(Audio.AudioColumns.GENRE); 3455 path = initialValues.getAsString(MediaStore.MediaColumns.DATA); 3456 } 3457 3458 3459 Uri newUri = null; 3460 DatabaseHelper helper = getDatabaseForUri(uri); 3461 if (helper == null && match != VOLUMES && match != MTP_CONNECTED) { 3462 throw new UnsupportedOperationException( 3463 "Unknown URI: " + uri); 3464 } 3465 3466 SQLiteDatabase db = ((match == VOLUMES || match == MTP_CONNECTED) ? null 3467 : helper.getWritableDatabase()); 3468 3469 switch (match) { 3470 case IMAGES_MEDIA: { 3471 rowId = insertFile(helper, uri, initialValues, 3472 FileColumns.MEDIA_TYPE_IMAGE, true, notifyRowIds); 3473 if (rowId > 0) { 3474 MediaDocumentsProvider.onMediaStoreInsert( 3475 getContext(), volumeName, FileColumns.MEDIA_TYPE_IMAGE, rowId); 3476 newUri = ContentUris.withAppendedId( 3477 Images.Media.getContentUri(volumeName), rowId); 3478 } 3479 break; 3480 } 3481 3482 // This will be triggered by requestMediaThumbnail (see getThumbnailUri) 3483 case IMAGES_THUMBNAILS: { 3484 ContentValues values = ensureFile(helper.mInternal, initialValues, ".jpg", 3485 "DCIM/.thumbnails"); 3486 helper.mNumInserts++; 3487 rowId = db.insert("thumbnails", "name", values); 3488 if (rowId > 0) { 3489 newUri = ContentUris.withAppendedId(Images.Thumbnails. 3490 getContentUri(volumeName), rowId); 3491 } 3492 break; 3493 } 3494 3495 // This is currently only used by MICRO_KIND video thumbnail (see getThumbnailUri) 3496 case VIDEO_THUMBNAILS: { 3497 ContentValues values = ensureFile(helper.mInternal, initialValues, ".jpg", 3498 "DCIM/.thumbnails"); 3499 helper.mNumInserts++; 3500 rowId = db.insert("videothumbnails", "name", values); 3501 if (rowId > 0) { 3502 newUri = ContentUris.withAppendedId(Video.Thumbnails. 3503 getContentUri(volumeName), rowId); 3504 } 3505 break; 3506 } 3507 3508 case AUDIO_MEDIA: { 3509 rowId = insertFile(helper, uri, initialValues, 3510 FileColumns.MEDIA_TYPE_AUDIO, true, notifyRowIds); 3511 if (rowId > 0) { 3512 MediaDocumentsProvider.onMediaStoreInsert( 3513 getContext(), volumeName, FileColumns.MEDIA_TYPE_AUDIO, rowId); 3514 newUri = ContentUris.withAppendedId( 3515 Audio.Media.getContentUri(volumeName), rowId); 3516 if (genre != null) { 3517 updateGenre(rowId, genre); 3518 } 3519 } 3520 break; 3521 } 3522 3523 case AUDIO_MEDIA_ID_GENRES: { 3524 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 3525 ContentValues values = new ContentValues(initialValues); 3526 values.put(Audio.Genres.Members.AUDIO_ID, audioId); 3527 helper.mNumInserts++; 3528 rowId = db.insert("audio_genres_map", "genre_id", values); 3529 if (rowId > 0) { 3530 newUri = ContentUris.withAppendedId(uri, rowId); 3531 } 3532 break; 3533 } 3534 3535 case AUDIO_MEDIA_ID_PLAYLISTS: { 3536 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 3537 ContentValues values = new ContentValues(initialValues); 3538 values.put(Audio.Playlists.Members.AUDIO_ID, audioId); 3539 helper.mNumInserts++; 3540 rowId = db.insert("audio_playlists_map", "playlist_id", 3541 values); 3542 if (rowId > 0) { 3543 newUri = ContentUris.withAppendedId(uri, rowId); 3544 } 3545 break; 3546 } 3547 3548 case AUDIO_GENRES: { 3549 helper.mNumInserts++; 3550 rowId = db.insert("audio_genres", "audio_id", initialValues); 3551 if (rowId > 0) { 3552 newUri = ContentUris.withAppendedId( 3553 Audio.Genres.getContentUri(volumeName), rowId); 3554 } 3555 break; 3556 } 3557 3558 case AUDIO_GENRES_ID_MEMBERS: { 3559 Long genreId = Long.parseLong(uri.getPathSegments().get(3)); 3560 ContentValues values = new ContentValues(initialValues); 3561 values.put(Audio.Genres.Members.GENRE_ID, genreId); 3562 helper.mNumInserts++; 3563 rowId = db.insert("audio_genres_map", "genre_id", values); 3564 if (rowId > 0) { 3565 newUri = ContentUris.withAppendedId(uri, rowId); 3566 } 3567 break; 3568 } 3569 3570 case AUDIO_PLAYLISTS: { 3571 ContentValues values = new ContentValues(initialValues); 3572 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 3573 rowId = insertFile(helper, uri, values, 3574 FileColumns.MEDIA_TYPE_PLAYLIST, true, notifyRowIds); 3575 if (rowId > 0) { 3576 newUri = ContentUris.withAppendedId( 3577 Audio.Playlists.getContentUri(volumeName), rowId); 3578 } 3579 break; 3580 } 3581 3582 case AUDIO_PLAYLISTS_ID: 3583 case AUDIO_PLAYLISTS_ID_MEMBERS: { 3584 Long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 3585 ContentValues values = new ContentValues(initialValues); 3586 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId); 3587 helper.mNumInserts++; 3588 rowId = db.insert("audio_playlists_map", "playlist_id", values); 3589 if (rowId > 0) { 3590 newUri = ContentUris.withAppendedId(uri, rowId); 3591 } 3592 break; 3593 } 3594 3595 case VIDEO_MEDIA: { 3596 rowId = insertFile(helper, uri, initialValues, 3597 FileColumns.MEDIA_TYPE_VIDEO, true, notifyRowIds); 3598 if (rowId > 0) { 3599 MediaDocumentsProvider.onMediaStoreInsert( 3600 getContext(), volumeName, FileColumns.MEDIA_TYPE_VIDEO, rowId); 3601 newUri = ContentUris.withAppendedId( 3602 Video.Media.getContentUri(volumeName), rowId); 3603 } 3604 break; 3605 } 3606 3607 case AUDIO_ALBUMART: { 3608 if (helper.mInternal) { 3609 throw new UnsupportedOperationException("no internal album art allowed"); 3610 } 3611 ContentValues values = null; 3612 try { 3613 values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 3614 } catch (IllegalStateException ex) { 3615 // probably no more room to store albumthumbs 3616 values = initialValues; 3617 } 3618 helper.mNumInserts++; 3619 rowId = db.insert("album_art", MediaStore.MediaColumns.DATA, values); 3620 if (rowId > 0) { 3621 newUri = ContentUris.withAppendedId(uri, rowId); 3622 } 3623 break; 3624 } 3625 3626 case VOLUMES: 3627 { 3628 String name = initialValues.getAsString("name"); 3629 Uri attachedVolume = attachVolume(name); 3630 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) { 3631 DatabaseHelper dbhelper = getDatabaseForUri(attachedVolume); 3632 if (dbhelper == null) { 3633 Log.e(TAG, "no database for attached volume " + attachedVolume); 3634 } else { 3635 dbhelper.mScanStartTime = SystemClock.currentTimeMicro(); 3636 } 3637 } 3638 return attachedVolume; 3639 } 3640 3641 case MTP_CONNECTED: 3642 synchronized (mMtpServiceConnection) { 3643 if (mMtpService == null) { 3644 Context context = getContext(); 3645 // MTP is connected, so grab a connection to MtpService 3646 context.bindService(new Intent(context, MtpService.class), 3647 mMtpServiceConnection, Context.BIND_AUTO_CREATE); 3648 } 3649 } 3650 break; 3651 3652 case FILES: 3653 rowId = insertFile(helper, uri, initialValues, 3654 FileColumns.MEDIA_TYPE_NONE, true, notifyRowIds); 3655 if (rowId > 0) { 3656 newUri = Files.getContentUri(volumeName, rowId); 3657 } 3658 break; 3659 3660 case MTP_OBJECTS: 3661 // We don't send a notification if the insert originated from MTP 3662 rowId = insertFile(helper, uri, initialValues, 3663 FileColumns.MEDIA_TYPE_NONE, false, notifyRowIds); 3664 if (rowId > 0) { 3665 newUri = Files.getMtpObjectsUri(volumeName, rowId); 3666 } 3667 break; 3668 3669 default: 3670 throw new UnsupportedOperationException("Invalid URI " + uri); 3671 } 3672 3673 if (path != null && path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 3674 // need to set the media_type of all the files below this folder to 0 3675 processNewNoMediaPath(helper, db, path); 3676 } 3677 return newUri; 3678 } 3679 3680 /* 3681 * Sets the media type of all files below the newly added .nomedia file or 3682 * hidden folder to 0, so the entries no longer appear in e.g. the audio and 3683 * images views. 3684 * 3685 * @param path The path to the new .nomedia file or hidden directory 3686 */ processNewNoMediaPath(final DatabaseHelper helper, final SQLiteDatabase db, final String path)3687 private void processNewNoMediaPath(final DatabaseHelper helper, final SQLiteDatabase db, 3688 final String path) { 3689 final File nomedia = new File(path); 3690 if (nomedia.exists()) { 3691 hidePath(helper, db, path); 3692 } else { 3693 // File doesn't exist. Try again in a little while. 3694 // XXX there's probably a better way of doing this 3695 new Thread(new Runnable() { 3696 @Override 3697 public void run() { 3698 SystemClock.sleep(2000); 3699 if (nomedia.exists()) { 3700 hidePath(helper, db, path); 3701 } else { 3702 Log.w(TAG, "does not exist: " + path, new Exception()); 3703 } 3704 }}).start(); 3705 } 3706 } 3707 hidePath(DatabaseHelper helper, SQLiteDatabase db, String path)3708 private void hidePath(DatabaseHelper helper, SQLiteDatabase db, String path) { 3709 // a new nomedia path was added, so clear the media paths 3710 MediaScanner.clearMediaPathCache(true /* media */, false /* nomedia */); 3711 File nomedia = new File(path); 3712 String hiddenroot = nomedia.isDirectory() ? path : nomedia.getParent(); 3713 ContentValues mediatype = new ContentValues(); 3714 mediatype.put("media_type", 0); 3715 int numrows = db.update("files", mediatype, 3716 "_data >= ? AND _data < ?", 3717 new String[] { hiddenroot + "/", hiddenroot + "0"}); 3718 helper.mNumUpdates += numrows; 3719 ContentResolver res = getContext().getContentResolver(); 3720 res.notifyChange(Uri.parse("content://media/"), null); 3721 } 3722 3723 /* 3724 * Rescan files for missing metadata and set their type accordingly. 3725 * There is code for detecting the removal of a nomedia file or renaming of 3726 * a directory from hidden to non-hidden in the MediaScanner and MtpDatabase, 3727 * both of which call here. 3728 */ processRemovedNoMediaPath(final String path)3729 private void processRemovedNoMediaPath(final String path) { 3730 // a nomedia path was removed, so clear the nomedia paths 3731 MediaScanner.clearMediaPathCache(false /* media */, true /* nomedia */); 3732 final DatabaseHelper helper; 3733 if (path.startsWith(mExternalStoragePaths[0])) { 3734 helper = getDatabaseForUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI); 3735 } else { 3736 helper = getDatabaseForUri(MediaStore.Audio.Media.INTERNAL_CONTENT_URI); 3737 } 3738 SQLiteDatabase db = helper.getWritableDatabase(); 3739 new ScannerClient(getContext(), db, path); 3740 } 3741 3742 private static final class ScannerClient implements MediaScannerConnectionClient { 3743 String mPath = null; 3744 MediaScannerConnection mScannerConnection; 3745 SQLiteDatabase mDb; 3746 ScannerClient(Context context, SQLiteDatabase db, String path)3747 public ScannerClient(Context context, SQLiteDatabase db, String path) { 3748 mDb = db; 3749 mPath = path; 3750 mScannerConnection = new MediaScannerConnection(context, this); 3751 mScannerConnection.connect(); 3752 } 3753 3754 @Override onMediaScannerConnected()3755 public void onMediaScannerConnected() { 3756 Cursor c = mDb.query("files", openFileColumns, 3757 "_data >= ? AND _data < ?", 3758 new String[] { mPath + "/", mPath + "0"}, 3759 null, null, null); 3760 try { 3761 while (c.moveToNext()) { 3762 String d = c.getString(0); 3763 File f = new File(d); 3764 if (f.isFile()) { 3765 mScannerConnection.scanFile(d, null); 3766 } 3767 } 3768 mScannerConnection.disconnect(); 3769 } finally { 3770 IoUtils.closeQuietly(c); 3771 } 3772 } 3773 3774 @Override onScanCompleted(String path, Uri uri)3775 public void onScanCompleted(String path, Uri uri) { 3776 } 3777 } 3778 3779 @Override applyBatch(ArrayList<ContentProviderOperation> operations)3780 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 3781 throws OperationApplicationException { 3782 3783 // The operations array provides no overall information about the URI(s) being operated 3784 // on, so begin a transaction for ALL of the databases. 3785 DatabaseHelper ihelper = getDatabaseForUri(MediaStore.Audio.Media.INTERNAL_CONTENT_URI); 3786 DatabaseHelper ehelper = getDatabaseForUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI); 3787 SQLiteDatabase idb = ihelper.getWritableDatabase(); 3788 idb.beginTransaction(); 3789 SQLiteDatabase edb = null; 3790 if (ehelper != null) { 3791 edb = ehelper.getWritableDatabase(); 3792 edb.beginTransaction(); 3793 } 3794 try { 3795 ContentProviderResult[] result = super.applyBatch(operations); 3796 idb.setTransactionSuccessful(); 3797 if (edb != null) { 3798 edb.setTransactionSuccessful(); 3799 } 3800 // Rather than sending targeted change notifications for every Uri 3801 // affected by the batch operation, just invalidate the entire internal 3802 // and external name space. 3803 ContentResolver res = getContext().getContentResolver(); 3804 res.notifyChange(Uri.parse("content://media/"), null); 3805 return result; 3806 } finally { 3807 idb.endTransaction(); 3808 if (edb != null) { 3809 edb.endTransaction(); 3810 } 3811 } 3812 } 3813 requestMediaThumbnail(String path, Uri uri, int priority, long magic)3814 private MediaThumbRequest requestMediaThumbnail(String path, Uri uri, int priority, long magic) { 3815 synchronized (mMediaThumbQueue) { 3816 MediaThumbRequest req = null; 3817 try { 3818 req = new MediaThumbRequest( 3819 getContext().getContentResolver(), path, uri, priority, magic); 3820 mMediaThumbQueue.add(req); 3821 // Trigger the handler. 3822 Message msg = mThumbHandler.obtainMessage(IMAGE_THUMB); 3823 msg.sendToTarget(); 3824 } catch (Throwable t) { 3825 Log.w(TAG, t); 3826 } 3827 return req; 3828 } 3829 } 3830 generateFileName(boolean internal, String preferredExtension, String directoryName)3831 private String generateFileName(boolean internal, String preferredExtension, String directoryName) 3832 { 3833 // create a random file 3834 String name = String.valueOf(System.currentTimeMillis()); 3835 3836 if (internal) { 3837 throw new UnsupportedOperationException("Writing to internal storage is not supported."); 3838 // return Environment.getDataDirectory() 3839 // + "/" + directoryName + "/" + name + preferredExtension; 3840 } else { 3841 String dirPath = mExternalStoragePaths[0] + "/" + directoryName; 3842 File dirFile = new File(dirPath); 3843 dirFile.mkdirs(); 3844 return dirPath + "/" + name + preferredExtension; 3845 } 3846 } 3847 ensureFileExists(Uri uri, String path)3848 private boolean ensureFileExists(Uri uri, String path) { 3849 File file = new File(path); 3850 if (file.exists()) { 3851 return true; 3852 } else { 3853 try { 3854 checkAccess(uri, file, 3855 ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE); 3856 } catch (FileNotFoundException e) { 3857 return false; 3858 } 3859 // we will not attempt to create the first directory in the path 3860 // (for example, do not create /sdcard if the SD card is not mounted) 3861 int secondSlash = path.indexOf('/', 1); 3862 if (secondSlash < 1) return false; 3863 String directoryPath = path.substring(0, secondSlash); 3864 File directory = new File(directoryPath); 3865 if (!directory.exists()) 3866 return false; 3867 file.getParentFile().mkdirs(); 3868 try { 3869 return file.createNewFile(); 3870 } catch(IOException ioe) { 3871 Log.e(TAG, "File creation failed", ioe); 3872 } 3873 return false; 3874 } 3875 } 3876 3877 private static final class GetTableAndWhereOutParameter { 3878 public String table; 3879 public String where; 3880 } 3881 3882 static final GetTableAndWhereOutParameter sGetTableAndWhereParam = 3883 new GetTableAndWhereOutParameter(); 3884 getTableAndWhere(Uri uri, int match, String userWhere, GetTableAndWhereOutParameter out)3885 private void getTableAndWhere(Uri uri, int match, String userWhere, 3886 GetTableAndWhereOutParameter out) { 3887 String where = null; 3888 switch (match) { 3889 case IMAGES_MEDIA: 3890 out.table = "files"; 3891 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_IMAGE; 3892 break; 3893 3894 case IMAGES_MEDIA_ID: 3895 out.table = "files"; 3896 where = "_id = " + uri.getPathSegments().get(3); 3897 break; 3898 3899 case IMAGES_THUMBNAILS_ID: 3900 where = "_id=" + uri.getPathSegments().get(3); 3901 case IMAGES_THUMBNAILS: 3902 out.table = "thumbnails"; 3903 break; 3904 3905 case AUDIO_MEDIA: 3906 out.table = "files"; 3907 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_AUDIO; 3908 break; 3909 3910 case AUDIO_MEDIA_ID: 3911 out.table = "files"; 3912 where = "_id=" + uri.getPathSegments().get(3); 3913 break; 3914 3915 case AUDIO_MEDIA_ID_GENRES: 3916 out.table = "audio_genres"; 3917 where = "audio_id=" + uri.getPathSegments().get(3); 3918 break; 3919 3920 case AUDIO_MEDIA_ID_GENRES_ID: 3921 out.table = "audio_genres"; 3922 where = "audio_id=" + uri.getPathSegments().get(3) + 3923 " AND genre_id=" + uri.getPathSegments().get(5); 3924 break; 3925 3926 case AUDIO_MEDIA_ID_PLAYLISTS: 3927 out.table = "audio_playlists"; 3928 where = "audio_id=" + uri.getPathSegments().get(3); 3929 break; 3930 3931 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 3932 out.table = "audio_playlists"; 3933 where = "audio_id=" + uri.getPathSegments().get(3) + 3934 " AND playlists_id=" + uri.getPathSegments().get(5); 3935 break; 3936 3937 case AUDIO_GENRES: 3938 out.table = "audio_genres"; 3939 break; 3940 3941 case AUDIO_GENRES_ID: 3942 out.table = "audio_genres"; 3943 where = "_id=" + uri.getPathSegments().get(3); 3944 break; 3945 3946 case AUDIO_GENRES_ID_MEMBERS: 3947 out.table = "audio_genres"; 3948 where = "genre_id=" + uri.getPathSegments().get(3); 3949 break; 3950 3951 case AUDIO_PLAYLISTS: 3952 out.table = "files"; 3953 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_PLAYLIST; 3954 break; 3955 3956 case AUDIO_PLAYLISTS_ID: 3957 out.table = "files"; 3958 where = "_id=" + uri.getPathSegments().get(3); 3959 break; 3960 3961 case AUDIO_PLAYLISTS_ID_MEMBERS: 3962 out.table = "audio_playlists_map"; 3963 where = "playlist_id=" + uri.getPathSegments().get(3); 3964 break; 3965 3966 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 3967 out.table = "audio_playlists_map"; 3968 where = "playlist_id=" + uri.getPathSegments().get(3) + 3969 " AND _id=" + uri.getPathSegments().get(5); 3970 break; 3971 3972 case AUDIO_ALBUMART_ID: 3973 out.table = "album_art"; 3974 where = "album_id=" + uri.getPathSegments().get(3); 3975 break; 3976 3977 case VIDEO_MEDIA: 3978 out.table = "files"; 3979 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_VIDEO; 3980 break; 3981 3982 case VIDEO_MEDIA_ID: 3983 out.table = "files"; 3984 where = "_id=" + uri.getPathSegments().get(3); 3985 break; 3986 3987 case VIDEO_THUMBNAILS_ID: 3988 where = "_id=" + uri.getPathSegments().get(3); 3989 case VIDEO_THUMBNAILS: 3990 out.table = "videothumbnails"; 3991 break; 3992 3993 case FILES_ID: 3994 case MTP_OBJECTS_ID: 3995 where = "_id=" + uri.getPathSegments().get(2); 3996 case FILES: 3997 case MTP_OBJECTS: 3998 out.table = "files"; 3999 break; 4000 4001 default: 4002 throw new UnsupportedOperationException( 4003 "Unknown or unsupported URL: " + uri.toString()); 4004 } 4005 4006 // Add in the user requested WHERE clause, if needed 4007 if (!TextUtils.isEmpty(userWhere)) { 4008 if (!TextUtils.isEmpty(where)) { 4009 out.where = where + " AND (" + userWhere + ")"; 4010 } else { 4011 out.where = userWhere; 4012 } 4013 } else { 4014 out.where = where; 4015 } 4016 } 4017 4018 @Override delete(Uri uri, String userWhere, String[] whereArgs)4019 public int delete(Uri uri, String userWhere, String[] whereArgs) { 4020 uri = safeUncanonicalize(uri); 4021 int count; 4022 int match = URI_MATCHER.match(uri); 4023 4024 // handle MEDIA_SCANNER before calling getDatabaseForUri() 4025 if (match == MEDIA_SCANNER) { 4026 if (mMediaScannerVolume == null) { 4027 return 0; 4028 } 4029 DatabaseHelper database = getDatabaseForUri( 4030 Uri.parse("content://media/" + mMediaScannerVolume + "/audio")); 4031 if (database == null) { 4032 Log.w(TAG, "no database for scanned volume " + mMediaScannerVolume); 4033 } else { 4034 database.mScanStopTime = SystemClock.currentTimeMicro(); 4035 String msg = dump(database, false); 4036 logToDb(database.getWritableDatabase(), msg); 4037 } 4038 mMediaScannerVolume = null; 4039 return 1; 4040 } 4041 4042 if (match == VOLUMES_ID) { 4043 detachVolume(uri); 4044 count = 1; 4045 } else if (match == MTP_CONNECTED) { 4046 synchronized (mMtpServiceConnection) { 4047 if (mMtpService != null) { 4048 // MTP has disconnected, so release our connection to MtpService 4049 getContext().unbindService(mMtpServiceConnection); 4050 count = 1; 4051 // mMtpServiceConnection.onServiceDisconnected might not get called, 4052 // so set mMtpService = null here 4053 mMtpService = null; 4054 } else { 4055 count = 0; 4056 } 4057 } 4058 } else { 4059 final String volumeName = getVolumeName(uri); 4060 4061 DatabaseHelper database = getDatabaseForUri(uri); 4062 if (database == null) { 4063 throw new UnsupportedOperationException( 4064 "Unknown URI: " + uri + " match: " + match); 4065 } 4066 database.mNumDeletes++; 4067 SQLiteDatabase db = database.getWritableDatabase(); 4068 4069 synchronized (sGetTableAndWhereParam) { 4070 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 4071 if (sGetTableAndWhereParam.table.equals("files")) { 4072 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA); 4073 if (deleteparam == null || ! deleteparam.equals("false")) { 4074 database.mNumQueries++; 4075 Cursor c = db.query(sGetTableAndWhereParam.table, 4076 sMediaTypeDataId, 4077 sGetTableAndWhereParam.where, whereArgs, null, null, null); 4078 String [] idvalue = new String[] { "" }; 4079 String [] playlistvalues = new String[] { "", "" }; 4080 try { 4081 while (c.moveToNext()) { 4082 final int mediaType = c.getInt(0); 4083 final String data = c.getString(1); 4084 final long id = c.getLong(2); 4085 4086 if (mediaType == FileColumns.MEDIA_TYPE_IMAGE) { 4087 deleteIfAllowed(uri, data); 4088 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4089 volumeName, FileColumns.MEDIA_TYPE_IMAGE, id); 4090 4091 idvalue[0] = String.valueOf(id); 4092 database.mNumQueries++; 4093 Cursor cc = db.query("thumbnails", sDataOnlyColumn, 4094 "image_id=?", idvalue, null, null, null); 4095 try { 4096 while (cc.moveToNext()) { 4097 deleteIfAllowed(uri, cc.getString(0)); 4098 } 4099 database.mNumDeletes++; 4100 db.delete("thumbnails", "image_id=?", idvalue); 4101 } finally { 4102 IoUtils.closeQuietly(cc); 4103 } 4104 } else if (mediaType == FileColumns.MEDIA_TYPE_VIDEO) { 4105 deleteIfAllowed(uri, data); 4106 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4107 volumeName, FileColumns.MEDIA_TYPE_VIDEO, id); 4108 4109 } else if (mediaType == FileColumns.MEDIA_TYPE_AUDIO) { 4110 if (!database.mInternal) { 4111 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4112 volumeName, FileColumns.MEDIA_TYPE_AUDIO, id); 4113 4114 idvalue[0] = String.valueOf(id); 4115 database.mNumDeletes += 2; // also count the one below 4116 db.delete("audio_genres_map", "audio_id=?", idvalue); 4117 // for each playlist that the item appears in, move 4118 // all the items behind it forward by one 4119 Cursor cc = db.query("audio_playlists_map", 4120 sPlaylistIdPlayOrder, 4121 "audio_id=?", idvalue, null, null, null); 4122 try { 4123 while (cc.moveToNext()) { 4124 playlistvalues[0] = "" + cc.getLong(0); 4125 playlistvalues[1] = "" + cc.getInt(1); 4126 database.mNumUpdates++; 4127 db.execSQL("UPDATE audio_playlists_map" + 4128 " SET play_order=play_order-1" + 4129 " WHERE playlist_id=? AND play_order>?", 4130 playlistvalues); 4131 } 4132 db.delete("audio_playlists_map", "audio_id=?", idvalue); 4133 } finally { 4134 IoUtils.closeQuietly(cc); 4135 } 4136 } 4137 } else if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 4138 // TODO, maybe: remove the audio_playlists_cleanup trigger and 4139 // implement functionality here (clean up the playlist map) 4140 } 4141 } 4142 } finally { 4143 IoUtils.closeQuietly(c); 4144 } 4145 } 4146 } 4147 4148 switch (match) { 4149 case MTP_OBJECTS: 4150 case MTP_OBJECTS_ID: 4151 try { 4152 // don't send objectRemoved event since this originated from MTP 4153 mDisableMtpObjectCallbacks = true; 4154 database.mNumDeletes++; 4155 count = db.delete("files", sGetTableAndWhereParam.where, whereArgs); 4156 } finally { 4157 mDisableMtpObjectCallbacks = false; 4158 } 4159 break; 4160 case AUDIO_GENRES_ID_MEMBERS: 4161 database.mNumDeletes++; 4162 count = db.delete("audio_genres_map", 4163 sGetTableAndWhereParam.where, whereArgs); 4164 break; 4165 4166 case IMAGES_THUMBNAILS_ID: 4167 case IMAGES_THUMBNAILS: 4168 case VIDEO_THUMBNAILS_ID: 4169 case VIDEO_THUMBNAILS: 4170 // Delete the referenced files first. 4171 Cursor c = db.query(sGetTableAndWhereParam.table, 4172 sDataOnlyColumn, 4173 sGetTableAndWhereParam.where, whereArgs, null, null, null); 4174 if (c != null) { 4175 try { 4176 while (c.moveToNext()) { 4177 deleteIfAllowed(uri, c.getString(0)); 4178 } 4179 } finally { 4180 IoUtils.closeQuietly(c); 4181 } 4182 } 4183 database.mNumDeletes++; 4184 count = db.delete(sGetTableAndWhereParam.table, 4185 sGetTableAndWhereParam.where, whereArgs); 4186 break; 4187 4188 default: 4189 database.mNumDeletes++; 4190 count = db.delete(sGetTableAndWhereParam.table, 4191 sGetTableAndWhereParam.where, whereArgs); 4192 break; 4193 } 4194 4195 // Since there are multiple Uris that can refer to the same files 4196 // and deletes can affect other objects in storage (like subdirectories 4197 // or playlists) we will notify a change on the entire volume to make 4198 // sure no listeners miss the notification. 4199 Uri notifyUri = Uri.parse("content://" + MediaStore.AUTHORITY + "/" + volumeName); 4200 getContext().getContentResolver().notifyChange(notifyUri, null); 4201 } 4202 } 4203 4204 return count; 4205 } 4206 4207 @Override call(String method, String arg, Bundle extras)4208 public Bundle call(String method, String arg, Bundle extras) { 4209 if (MediaStore.UNHIDE_CALL.equals(method)) { 4210 processRemovedNoMediaPath(arg); 4211 return null; 4212 } 4213 throw new UnsupportedOperationException("Unsupported call: " + method); 4214 } 4215 4216 @Override update(Uri uri, ContentValues initialValues, String userWhere, String[] whereArgs)4217 public int update(Uri uri, ContentValues initialValues, String userWhere, 4218 String[] whereArgs) { 4219 uri = safeUncanonicalize(uri); 4220 int count; 4221 // Log.v(TAG, "update for uri="+uri+", initValues="+initialValues); 4222 int match = URI_MATCHER.match(uri); 4223 DatabaseHelper helper = getDatabaseForUri(uri); 4224 if (helper == null) { 4225 throw new UnsupportedOperationException( 4226 "Unknown URI: " + uri); 4227 } 4228 helper.mNumUpdates++; 4229 4230 SQLiteDatabase db = helper.getWritableDatabase(); 4231 4232 String genre = null; 4233 if (initialValues != null) { 4234 genre = initialValues.getAsString(Audio.AudioColumns.GENRE); 4235 initialValues.remove(Audio.AudioColumns.GENRE); 4236 } 4237 4238 synchronized (sGetTableAndWhereParam) { 4239 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 4240 4241 // special case renaming directories via MTP. 4242 // in this case we must update all paths in the database with 4243 // the directory name as a prefix 4244 if ((match == MTP_OBJECTS || match == MTP_OBJECTS_ID) 4245 && initialValues != null && initialValues.size() == 1) { 4246 String oldPath = null; 4247 String newPath = initialValues.getAsString(MediaStore.MediaColumns.DATA); 4248 mDirectoryCache.remove(newPath); 4249 // MtpDatabase will rename the directory first, so we test the new file name 4250 File f = new File(newPath); 4251 if (newPath != null && f.isDirectory()) { 4252 helper.mNumQueries++; 4253 Cursor cursor = db.query(sGetTableAndWhereParam.table, PATH_PROJECTION, 4254 userWhere, whereArgs, null, null, null); 4255 try { 4256 if (cursor != null && cursor.moveToNext()) { 4257 oldPath = cursor.getString(1); 4258 } 4259 } finally { 4260 IoUtils.closeQuietly(cursor); 4261 } 4262 if (oldPath != null) { 4263 mDirectoryCache.remove(oldPath); 4264 // first rename the row for the directory 4265 helper.mNumUpdates++; 4266 count = db.update(sGetTableAndWhereParam.table, initialValues, 4267 sGetTableAndWhereParam.where, whereArgs); 4268 if (count > 0) { 4269 // update the paths of any files and folders contained in the directory 4270 Object[] bindArgs = new Object[] { 4271 newPath, 4272 oldPath.length() + 1, 4273 oldPath + "/", 4274 oldPath + "0", 4275 // update bucket_display_name and bucket_id based on new path 4276 f.getName(), 4277 f.toString().toLowerCase().hashCode() 4278 }; 4279 helper.mNumUpdates++; 4280 db.execSQL("UPDATE files SET _data=?1||SUBSTR(_data, ?2)" + 4281 // also update bucket_display_name 4282 ",bucket_display_name=?5" + 4283 ",bucket_id=?6" + 4284 " WHERE _data >= ?3 AND _data < ?4;", 4285 bindArgs); 4286 } 4287 4288 if (count > 0 && !db.inTransaction()) { 4289 getContext().getContentResolver().notifyChange(uri, null); 4290 } 4291 if (f.getName().startsWith(".")) { 4292 // the new directory name is hidden 4293 processNewNoMediaPath(helper, db, newPath); 4294 } 4295 return count; 4296 } 4297 } else if (newPath.toLowerCase(Locale.US).endsWith("/.nomedia")) { 4298 processNewNoMediaPath(helper, db, newPath); 4299 } 4300 } 4301 4302 switch (match) { 4303 case AUDIO_MEDIA: 4304 case AUDIO_MEDIA_ID: 4305 { 4306 ContentValues values = new ContentValues(initialValues); 4307 String albumartist = values.getAsString(MediaStore.Audio.Media.ALBUM_ARTIST); 4308 String compilation = values.getAsString(MediaStore.Audio.Media.COMPILATION); 4309 values.remove(MediaStore.Audio.Media.COMPILATION); 4310 4311 // Insert the artist into the artist table and remove it from 4312 // the input values 4313 String artist = values.getAsString("artist"); 4314 values.remove("artist"); 4315 if (artist != null) { 4316 long artistRowId; 4317 HashMap<String, Long> artistCache = helper.mArtistCache; 4318 synchronized(artistCache) { 4319 Long temp = artistCache.get(artist); 4320 if (temp == null) { 4321 artistRowId = getKeyIdForName(helper, db, 4322 "artists", "artist_key", "artist", 4323 artist, artist, null, 0, null, artistCache, uri); 4324 } else { 4325 artistRowId = temp.longValue(); 4326 } 4327 } 4328 values.put("artist_id", Integer.toString((int)artistRowId)); 4329 } 4330 4331 // Do the same for the album field. 4332 String so = values.getAsString("album"); 4333 values.remove("album"); 4334 if (so != null) { 4335 String path = values.getAsString(MediaStore.MediaColumns.DATA); 4336 int albumHash = 0; 4337 if (albumartist != null) { 4338 albumHash = albumartist.hashCode(); 4339 } else if (compilation != null && compilation.equals("1")) { 4340 // nothing to do, hash already set 4341 } else { 4342 if (path == null) { 4343 if (match == AUDIO_MEDIA) { 4344 Log.w(TAG, "Possible multi row album name update without" 4345 + " path could give wrong album key"); 4346 } else { 4347 //Log.w(TAG, "Specify path to avoid extra query"); 4348 Cursor c = query(uri, 4349 new String[] { MediaStore.Audio.Media.DATA}, 4350 null, null, null); 4351 if (c != null) { 4352 try { 4353 int numrows = c.getCount(); 4354 if (numrows == 1) { 4355 c.moveToFirst(); 4356 path = c.getString(0); 4357 } else { 4358 Log.e(TAG, "" + numrows + " rows for " + uri); 4359 } 4360 } finally { 4361 IoUtils.closeQuietly(c); 4362 } 4363 } 4364 } 4365 } 4366 if (path != null) { 4367 albumHash = path.substring(0, path.lastIndexOf('/')).hashCode(); 4368 } 4369 } 4370 4371 String s = so.toString(); 4372 long albumRowId; 4373 HashMap<String, Long> albumCache = helper.mAlbumCache; 4374 synchronized(albumCache) { 4375 String cacheName = s + albumHash; 4376 Long temp = albumCache.get(cacheName); 4377 if (temp == null) { 4378 albumRowId = getKeyIdForName(helper, db, 4379 "albums", "album_key", "album", 4380 s, cacheName, path, albumHash, artist, albumCache, uri); 4381 } else { 4382 albumRowId = temp.longValue(); 4383 } 4384 } 4385 values.put("album_id", Integer.toString((int)albumRowId)); 4386 } 4387 4388 // don't allow the title_key field to be updated directly 4389 values.remove("title_key"); 4390 // If the title field is modified, update the title_key 4391 so = values.getAsString("title"); 4392 if (so != null) { 4393 String s = so.toString(); 4394 values.put("title_key", MediaStore.Audio.keyFor(s)); 4395 // do a final trim of the title, in case it started with the special 4396 // "sort first" character (ascii \001) 4397 values.remove("title"); 4398 values.put("title", s.trim()); 4399 } 4400 4401 helper.mNumUpdates++; 4402 count = db.update(sGetTableAndWhereParam.table, values, 4403 sGetTableAndWhereParam.where, whereArgs); 4404 if (genre != null) { 4405 if (count == 1 && match == AUDIO_MEDIA_ID) { 4406 long rowId = Long.parseLong(uri.getPathSegments().get(3)); 4407 updateGenre(rowId, genre); 4408 } else { 4409 // can't handle genres for bulk update or for non-audio files 4410 Log.w(TAG, "ignoring genre in update: count = " 4411 + count + " match = " + match); 4412 } 4413 } 4414 } 4415 break; 4416 case IMAGES_MEDIA: 4417 case IMAGES_MEDIA_ID: 4418 case VIDEO_MEDIA: 4419 case VIDEO_MEDIA_ID: 4420 { 4421 ContentValues values = new ContentValues(initialValues); 4422 // Don't allow bucket id or display name to be updated directly. 4423 // The same names are used for both images and table columns, so 4424 // we use the ImageColumns constants here. 4425 values.remove(ImageColumns.BUCKET_ID); 4426 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 4427 // If the data is being modified update the bucket values 4428 String data = values.getAsString(MediaColumns.DATA); 4429 if (data != null) { 4430 computeBucketValues(data, values); 4431 } 4432 computeTakenTime(values); 4433 helper.mNumUpdates++; 4434 count = db.update(sGetTableAndWhereParam.table, values, 4435 sGetTableAndWhereParam.where, whereArgs); 4436 // if this is a request from MediaScanner, DATA should contains file path 4437 // we only process update request from media scanner, otherwise the requests 4438 // could be duplicate. 4439 if (count > 0 && values.getAsString(MediaStore.MediaColumns.DATA) != null) { 4440 helper.mNumQueries++; 4441 Cursor c = db.query(sGetTableAndWhereParam.table, 4442 READY_FLAG_PROJECTION, sGetTableAndWhereParam.where, 4443 whereArgs, null, null, null); 4444 if (c != null) { 4445 try { 4446 while (c.moveToNext()) { 4447 long magic = c.getLong(2); 4448 if (magic == 0) { 4449 requestMediaThumbnail(c.getString(1), uri, 4450 MediaThumbRequest.PRIORITY_NORMAL, 0); 4451 } 4452 } 4453 } finally { 4454 IoUtils.closeQuietly(c); 4455 } 4456 } 4457 } 4458 } 4459 break; 4460 4461 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 4462 String moveit = uri.getQueryParameter("move"); 4463 if (moveit != null) { 4464 String key = MediaStore.Audio.Playlists.Members.PLAY_ORDER; 4465 if (initialValues.containsKey(key)) { 4466 int newpos = initialValues.getAsInteger(key); 4467 List <String> segments = uri.getPathSegments(); 4468 long playlist = Long.parseLong(segments.get(3)); 4469 int oldpos = Integer.parseInt(segments.get(5)); 4470 return movePlaylistEntry(helper, db, playlist, oldpos, newpos); 4471 } 4472 throw new IllegalArgumentException("Need to specify " + key + 4473 " when using 'move' parameter"); 4474 } 4475 // fall through 4476 default: 4477 helper.mNumUpdates++; 4478 count = db.update(sGetTableAndWhereParam.table, initialValues, 4479 sGetTableAndWhereParam.where, whereArgs); 4480 break; 4481 } 4482 } 4483 // in a transaction, the code that began the transaction should be taking 4484 // care of notifications once it ends the transaction successfully 4485 if (count > 0 && !db.inTransaction()) { 4486 getContext().getContentResolver().notifyChange(uri, null); 4487 } 4488 return count; 4489 } 4490 movePlaylistEntry(DatabaseHelper helper, SQLiteDatabase db, long playlist, int from, int to)4491 private int movePlaylistEntry(DatabaseHelper helper, SQLiteDatabase db, 4492 long playlist, int from, int to) { 4493 if (from == to) { 4494 return 0; 4495 } 4496 db.beginTransaction(); 4497 int numlines = 0; 4498 Cursor c = null; 4499 try { 4500 helper.mNumUpdates += 3; 4501 c = db.query("audio_playlists_map", 4502 new String [] {"play_order" }, 4503 "playlist_id=?", new String[] {"" + playlist}, null, null, "play_order", 4504 from + ",1"); 4505 c.moveToFirst(); 4506 int from_play_order = c.getInt(0); 4507 IoUtils.closeQuietly(c); 4508 c = db.query("audio_playlists_map", 4509 new String [] {"play_order" }, 4510 "playlist_id=?", new String[] {"" + playlist}, null, null, "play_order", 4511 to + ",1"); 4512 c.moveToFirst(); 4513 int to_play_order = c.getInt(0); 4514 db.execSQL("UPDATE audio_playlists_map SET play_order=-1" + 4515 " WHERE play_order=" + from_play_order + 4516 " AND playlist_id=" + playlist); 4517 // We could just run both of the next two statements, but only one of 4518 // of them will actually do anything, so might as well skip the compile 4519 // and execute steps. 4520 if (from < to) { 4521 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order-1" + 4522 " WHERE play_order<=" + to_play_order + 4523 " AND play_order>" + from_play_order + 4524 " AND playlist_id=" + playlist); 4525 numlines = to - from + 1; 4526 } else { 4527 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order+1" + 4528 " WHERE play_order>=" + to_play_order + 4529 " AND play_order<" + from_play_order + 4530 " AND playlist_id=" + playlist); 4531 numlines = from - to + 1; 4532 } 4533 db.execSQL("UPDATE audio_playlists_map SET play_order=" + to_play_order + 4534 " WHERE play_order=-1 AND playlist_id=" + playlist); 4535 db.setTransactionSuccessful(); 4536 } finally { 4537 db.endTransaction(); 4538 IoUtils.closeQuietly(c); 4539 } 4540 4541 Uri uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI 4542 .buildUpon().appendEncodedPath(String.valueOf(playlist)).build(); 4543 // notifyChange() must be called after the database transaction is ended 4544 // or the listeners will read the old data in the callback 4545 getContext().getContentResolver().notifyChange(uri, null); 4546 4547 return numlines; 4548 } 4549 4550 private static final String[] openFileColumns = new String[] { 4551 MediaStore.MediaColumns.DATA, 4552 }; 4553 4554 @Override openFile(Uri uri, String mode)4555 public ParcelFileDescriptor openFile(Uri uri, String mode) 4556 throws FileNotFoundException { 4557 4558 uri = safeUncanonicalize(uri); 4559 ParcelFileDescriptor pfd = null; 4560 4561 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_FILE_ID) { 4562 // get album art for the specified media file 4563 DatabaseHelper database = getDatabaseForUri(uri); 4564 if (database == null) { 4565 throw new IllegalStateException("Couldn't open database for " + uri); 4566 } 4567 SQLiteDatabase db = database.getReadableDatabase(); 4568 if (db == null) { 4569 throw new IllegalStateException("Couldn't open database for " + uri); 4570 } 4571 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4572 int songid = Integer.parseInt(uri.getPathSegments().get(3)); 4573 qb.setTables("audio_meta"); 4574 qb.appendWhere("_id=" + songid); 4575 Cursor c = qb.query(db, 4576 new String [] { 4577 MediaStore.Audio.Media.DATA, 4578 MediaStore.Audio.Media.ALBUM_ID }, 4579 null, null, null, null, null); 4580 try { 4581 if (c.moveToFirst()) { 4582 String audiopath = c.getString(0); 4583 int albumid = c.getInt(1); 4584 // Try to get existing album art for this album first, which 4585 // could possibly have been obtained from a different file. 4586 // If that fails, try to get it from this specific file. 4587 Uri newUri = ContentUris.withAppendedId(ALBUMART_URI, albumid); 4588 try { 4589 pfd = openFileAndEnforcePathPermissionsHelper(newUri, mode); 4590 } catch (FileNotFoundException ex) { 4591 // That didn't work, now try to get it from the specific file 4592 pfd = getThumb(database, db, audiopath, albumid, null); 4593 } 4594 } 4595 } finally { 4596 IoUtils.closeQuietly(c); 4597 } 4598 return pfd; 4599 } 4600 4601 try { 4602 pfd = openFileAndEnforcePathPermissionsHelper(uri, mode); 4603 } catch (FileNotFoundException ex) { 4604 if (mode.contains("w")) { 4605 // if the file couldn't be created, we shouldn't extract album art 4606 throw ex; 4607 } 4608 4609 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_ID) { 4610 // Tried to open an album art file which does not exist. Regenerate. 4611 DatabaseHelper database = getDatabaseForUri(uri); 4612 if (database == null) { 4613 throw ex; 4614 } 4615 SQLiteDatabase db = database.getReadableDatabase(); 4616 if (db == null) { 4617 throw new IllegalStateException("Couldn't open database for " + uri); 4618 } 4619 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4620 int albumid = Integer.parseInt(uri.getPathSegments().get(3)); 4621 qb.setTables("audio_meta"); 4622 qb.appendWhere("album_id=" + albumid); 4623 Cursor c = qb.query(db, 4624 new String [] { 4625 MediaStore.Audio.Media.DATA }, 4626 null, null, null, null, MediaStore.Audio.Media.TRACK); 4627 try { 4628 if (c.moveToFirst()) { 4629 String audiopath = c.getString(0); 4630 pfd = getThumb(database, db, audiopath, albumid, uri); 4631 } 4632 } finally { 4633 IoUtils.closeQuietly(c); 4634 } 4635 } 4636 if (pfd == null) { 4637 throw ex; 4638 } 4639 } 4640 return pfd; 4641 } 4642 4643 /** 4644 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 4645 */ queryForDataFile(Uri uri)4646 private File queryForDataFile(Uri uri) throws FileNotFoundException { 4647 final Cursor cursor = query( 4648 uri, new String[] { MediaColumns.DATA }, null, null, null); 4649 if (cursor == null) { 4650 throw new FileNotFoundException("Missing cursor for " + uri); 4651 } 4652 4653 try { 4654 switch (cursor.getCount()) { 4655 case 0: 4656 throw new FileNotFoundException("No entry for " + uri); 4657 case 1: 4658 if (cursor.moveToFirst()) { 4659 String data = cursor.getString(0); 4660 if (data == null) { 4661 throw new FileNotFoundException("Null path for " + uri); 4662 } 4663 return new File(data); 4664 } else { 4665 throw new FileNotFoundException("Unable to read entry for " + uri); 4666 } 4667 default: 4668 throw new FileNotFoundException("Multiple items at " + uri); 4669 } 4670 } finally { 4671 IoUtils.closeQuietly(cursor); 4672 } 4673 } 4674 4675 /** 4676 * Replacement for {@link #openFileHelper(Uri, String)} which enforces any 4677 * permissions applicable to the path before returning. 4678 */ openFileAndEnforcePathPermissionsHelper(Uri uri, String mode)4679 private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, String mode) 4680 throws FileNotFoundException { 4681 final int modeBits = ParcelFileDescriptor.parseMode(mode); 4682 4683 File file = queryForDataFile(uri); 4684 4685 checkAccess(uri, file, modeBits); 4686 4687 // Bypass emulation layer when file is opened for reading, but only 4688 // when opening read-only and we have an exact match. 4689 if (modeBits == MODE_READ_ONLY) { 4690 file = Environment.maybeTranslateEmulatedPathToInternal(file); 4691 } 4692 4693 return ParcelFileDescriptor.open(file, modeBits); 4694 } 4695 deleteIfAllowed(Uri uri, String path)4696 private void deleteIfAllowed(Uri uri, String path) { 4697 try { 4698 File file = new File(path); 4699 checkAccess(uri, file, ParcelFileDescriptor.MODE_WRITE_ONLY); 4700 file.delete(); 4701 } catch (Exception e) { 4702 Log.e(TAG, "Couldn't delete " + path); 4703 } 4704 } 4705 checkAccess(Uri uri, File file, int modeBits)4706 private void checkAccess(Uri uri, File file, int modeBits) throws FileNotFoundException { 4707 final boolean isWrite = (modeBits & MODE_WRITE_ONLY) != 0; 4708 final String path; 4709 try { 4710 path = file.getCanonicalPath(); 4711 } catch (IOException e) { 4712 throw new IllegalArgumentException("Unable to resolve canonical path for " + file, e); 4713 } 4714 4715 Context c = getContext(); 4716 boolean readGranted = false; 4717 boolean writeGranted = false; 4718 if (isWrite) { 4719 writeGranted = 4720 (c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 4721 == PackageManager.PERMISSION_GRANTED); 4722 } else { 4723 readGranted = 4724 (c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) 4725 == PackageManager.PERMISSION_GRANTED); 4726 } 4727 4728 if (path.startsWith(mExternalPath) || path.startsWith(mLegacyPath)) { 4729 if (isWrite) { 4730 if (!writeGranted) { 4731 enforceCallingOrSelfPermissionAndAppOps( 4732 WRITE_EXTERNAL_STORAGE, "External path: " + path); 4733 } 4734 } else if (!readGranted) { 4735 enforceCallingOrSelfPermissionAndAppOps( 4736 READ_EXTERNAL_STORAGE, "External path: " + path); 4737 } 4738 } else if (path.startsWith(mCachePath)) { 4739 if ((isWrite && !writeGranted) || !readGranted) { 4740 c.enforceCallingOrSelfPermission(ACCESS_CACHE_FILESYSTEM, "Cache path: " + path); 4741 } 4742 } else if (isSecondaryExternalPath(path)) { 4743 // read access is OK with the appropriate permission 4744 if (!readGranted) { 4745 if (c.checkCallingOrSelfPermission(WRITE_MEDIA_STORAGE) 4746 == PackageManager.PERMISSION_DENIED) { 4747 enforceCallingOrSelfPermissionAndAppOps( 4748 READ_EXTERNAL_STORAGE, "External path: " + path); 4749 } 4750 } 4751 if (isWrite) { 4752 if (c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 4753 != PackageManager.PERMISSION_GRANTED) { 4754 c.enforceCallingOrSelfPermission( 4755 WRITE_MEDIA_STORAGE, "External path: " + path); 4756 } 4757 } 4758 } else if (isWrite) { 4759 // don't write to non-cache, non-sdcard files. 4760 throw new FileNotFoundException("Can't access " + file); 4761 } else { 4762 checkWorldReadAccess(path); 4763 } 4764 } 4765 isSecondaryExternalPath(String path)4766 private boolean isSecondaryExternalPath(String path) { 4767 for (int i = 1; i < mExternalStoragePaths.length; i++) { 4768 if (path.startsWith(mExternalStoragePaths[i])) { 4769 return true; 4770 } 4771 } 4772 return false; 4773 } 4774 4775 /** 4776 * Check whether the path is a world-readable file 4777 */ checkWorldReadAccess(String path)4778 private static void checkWorldReadAccess(String path) throws FileNotFoundException { 4779 // Path has already been canonicalized, and we relax the check to look 4780 // at groups to support runtime storage permissions. 4781 final int accessBits = path.startsWith("/storage/") ? OsConstants.S_IRGRP 4782 : OsConstants.S_IROTH; 4783 try { 4784 StructStat stat = Os.stat(path); 4785 if (OsConstants.S_ISREG(stat.st_mode) && 4786 ((stat.st_mode & accessBits) == accessBits)) { 4787 checkLeadingPathComponentsWorldExecutable(path); 4788 return; 4789 } 4790 } catch (ErrnoException e) { 4791 // couldn't stat the file, either it doesn't exist or isn't 4792 // accessible to us 4793 } 4794 4795 throw new FileNotFoundException("Can't access " + path); 4796 } 4797 checkLeadingPathComponentsWorldExecutable(String filePath)4798 private static void checkLeadingPathComponentsWorldExecutable(String filePath) 4799 throws FileNotFoundException { 4800 File parent = new File(filePath).getParentFile(); 4801 4802 // Path has already been canonicalized, and we relax the check to look 4803 // at groups to support runtime storage permissions. 4804 final int accessBits = filePath.startsWith("/storage/") ? OsConstants.S_IXGRP 4805 : OsConstants.S_IXOTH; 4806 4807 while (parent != null) { 4808 if (! parent.exists()) { 4809 // parent dir doesn't exist, give up 4810 throw new FileNotFoundException("access denied"); 4811 } 4812 try { 4813 StructStat stat = Os.stat(parent.getPath()); 4814 if ((stat.st_mode & accessBits) != accessBits) { 4815 // the parent dir doesn't have the appropriate access 4816 throw new FileNotFoundException("Can't access " + filePath); 4817 } 4818 } catch (ErrnoException e1) { 4819 // couldn't stat() parent 4820 throw new FileNotFoundException("Can't access " + filePath); 4821 } 4822 parent = parent.getParentFile(); 4823 } 4824 } 4825 4826 private class ThumbData { 4827 DatabaseHelper helper; 4828 SQLiteDatabase db; 4829 String path; 4830 long album_id; 4831 Uri albumart_uri; 4832 } 4833 makeThumbAsync(DatabaseHelper helper, SQLiteDatabase db, String path, long album_id)4834 private void makeThumbAsync(DatabaseHelper helper, SQLiteDatabase db, 4835 String path, long album_id) { 4836 synchronized (mPendingThumbs) { 4837 if (mPendingThumbs.contains(path)) { 4838 // There's already a request to make an album art thumbnail 4839 // for this audio file in the queue. 4840 return; 4841 } 4842 4843 mPendingThumbs.add(path); 4844 } 4845 4846 ThumbData d = new ThumbData(); 4847 d.helper = helper; 4848 d.db = db; 4849 d.path = path; 4850 d.album_id = album_id; 4851 d.albumart_uri = ContentUris.withAppendedId(mAlbumArtBaseUri, album_id); 4852 4853 // Instead of processing thumbnail requests in the order they were 4854 // received we instead process them stack-based, i.e. LIFO. 4855 // The idea behind this is that the most recently requested thumbnails 4856 // are most likely the ones still in the user's view, whereas those 4857 // requested earlier may have already scrolled off. 4858 synchronized (mThumbRequestStack) { 4859 mThumbRequestStack.push(d); 4860 } 4861 4862 // Trigger the handler. 4863 Message msg = mThumbHandler.obtainMessage(ALBUM_THUMB); 4864 msg.sendToTarget(); 4865 } 4866 4867 //Return true if the artPath is the dir as it in mExternalStoragePaths 4868 //for multi storage support isRootStorageDir(String[] rootPaths, String testPath)4869 private static boolean isRootStorageDir(String[] rootPaths, String testPath) { 4870 for (String rootPath : rootPaths) { 4871 if (rootPath != null && rootPath.equalsIgnoreCase(testPath)) { 4872 return true; 4873 } 4874 } 4875 return false; 4876 } 4877 4878 // Extract compressed image data from the audio file itself or, if that fails, 4879 // look for a file "AlbumArt.jpg" in the containing directory. getCompressedAlbumArt(Context context, String[] rootPaths, String path)4880 private static byte[] getCompressedAlbumArt(Context context, String[] rootPaths, String path) { 4881 byte[] compressed = null; 4882 4883 try { 4884 File f = new File(path); 4885 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, 4886 ParcelFileDescriptor.MODE_READ_ONLY); 4887 4888 try (MediaScanner scanner = new MediaScanner(context, "internal")) { 4889 compressed = scanner.extractAlbumArt(pfd.getFileDescriptor()); 4890 } 4891 pfd.close(); 4892 4893 // If no embedded art exists, look for a suitable image file in the 4894 // same directory as the media file, except if that directory is 4895 // is the root directory of the sd card or the download directory. 4896 // We look for, in order of preference: 4897 // 0 AlbumArt.jpg 4898 // 1 AlbumArt*Large.jpg 4899 // 2 Any other jpg image with 'albumart' anywhere in the name 4900 // 3 Any other jpg image 4901 // 4 any other png image 4902 if (compressed == null && path != null) { 4903 int lastSlash = path.lastIndexOf('/'); 4904 if (lastSlash > 0) { 4905 4906 String artPath = path.substring(0, lastSlash); 4907 String dwndir = Environment.getExternalStoragePublicDirectory( 4908 Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); 4909 4910 String bestmatch = null; 4911 synchronized (sFolderArtMap) { 4912 if (sFolderArtMap.containsKey(artPath)) { 4913 bestmatch = sFolderArtMap.get(artPath); 4914 } else if (!isRootStorageDir(rootPaths, artPath) && 4915 !artPath.equalsIgnoreCase(dwndir)) { 4916 File dir = new File(artPath); 4917 String [] entrynames = dir.list(); 4918 if (entrynames == null) { 4919 return null; 4920 } 4921 bestmatch = null; 4922 int matchlevel = 1000; 4923 for (int i = entrynames.length - 1; i >=0; i--) { 4924 String entry = entrynames[i].toLowerCase(); 4925 if (entry.equals("albumart.jpg")) { 4926 bestmatch = entrynames[i]; 4927 break; 4928 } else if (entry.startsWith("albumart") 4929 && entry.endsWith("large.jpg") 4930 && matchlevel > 1) { 4931 bestmatch = entrynames[i]; 4932 matchlevel = 1; 4933 } else if (entry.contains("albumart") 4934 && entry.endsWith(".jpg") 4935 && matchlevel > 2) { 4936 bestmatch = entrynames[i]; 4937 matchlevel = 2; 4938 } else if (entry.endsWith(".jpg") && matchlevel > 3) { 4939 bestmatch = entrynames[i]; 4940 matchlevel = 3; 4941 } else if (entry.endsWith(".png") && matchlevel > 4) { 4942 bestmatch = entrynames[i]; 4943 matchlevel = 4; 4944 } 4945 } 4946 // note that this may insert null if no album art was found 4947 sFolderArtMap.put(artPath, bestmatch); 4948 } 4949 } 4950 4951 if (bestmatch != null) { 4952 File file = new File(artPath, bestmatch); 4953 if (file.exists()) { 4954 FileInputStream stream = null; 4955 try { 4956 compressed = new byte[(int)file.length()]; 4957 stream = new FileInputStream(file); 4958 stream.read(compressed); 4959 } catch (IOException ex) { 4960 compressed = null; 4961 } catch (OutOfMemoryError ex) { 4962 Log.w(TAG, ex); 4963 compressed = null; 4964 } finally { 4965 if (stream != null) { 4966 stream.close(); 4967 } 4968 } 4969 } 4970 } 4971 } 4972 } 4973 } catch (IOException e) { 4974 } 4975 4976 return compressed; 4977 } 4978 4979 // Return a URI to write the album art to and update the database as necessary. getAlbumArtOutputUri(DatabaseHelper helper, SQLiteDatabase db, long album_id, Uri albumart_uri)4980 Uri getAlbumArtOutputUri(DatabaseHelper helper, SQLiteDatabase db, long album_id, Uri albumart_uri) { 4981 Uri out = null; 4982 // TODO: this could be done more efficiently with a call to db.replace(), which 4983 // replaces or inserts as needed, making it unnecessary to query() first. 4984 if (albumart_uri != null) { 4985 Cursor c = query(albumart_uri, new String [] { MediaStore.MediaColumns.DATA }, 4986 null, null, null); 4987 try { 4988 if (c != null && c.moveToFirst()) { 4989 String albumart_path = c.getString(0); 4990 if (ensureFileExists(albumart_uri, albumart_path)) { 4991 out = albumart_uri; 4992 } 4993 } else { 4994 albumart_uri = null; 4995 } 4996 } finally { 4997 IoUtils.closeQuietly(c); 4998 } 4999 } 5000 if (albumart_uri == null){ 5001 ContentValues initialValues = new ContentValues(); 5002 initialValues.put("album_id", album_id); 5003 try { 5004 ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 5005 helper.mNumInserts++; 5006 long rowId = db.insert("album_art", MediaStore.MediaColumns.DATA, values); 5007 if (rowId > 0) { 5008 out = ContentUris.withAppendedId(ALBUMART_URI, rowId); 5009 // ensure the parent directory exists 5010 String albumart_path = values.getAsString(MediaStore.MediaColumns.DATA); 5011 ensureFileExists(out, albumart_path); 5012 } 5013 } catch (IllegalStateException ex) { 5014 Log.e(TAG, "error creating album thumb file"); 5015 } 5016 } 5017 return out; 5018 } 5019 5020 // Write out the album art to the output URI, recompresses the given Bitmap 5021 // if necessary, otherwise writes the compressed data. writeAlbumArt( boolean need_to_recompress, Uri out, byte[] compressed, Bitmap bm)5022 private void writeAlbumArt( 5023 boolean need_to_recompress, Uri out, byte[] compressed, Bitmap bm) throws IOException { 5024 OutputStream outstream = null; 5025 // Clear calling identity as we may be handling an IPC. 5026 final long identity = Binder.clearCallingIdentity(); 5027 try { 5028 outstream = getContext().getContentResolver().openOutputStream(out); 5029 5030 if (!need_to_recompress) { 5031 // No need to recompress here, just write out the original 5032 // compressed data here. 5033 outstream.write(compressed); 5034 } else { 5035 if (!bm.compress(Bitmap.CompressFormat.JPEG, 85, outstream)) { 5036 throw new IOException("failed to compress bitmap"); 5037 } 5038 } 5039 } finally { 5040 Binder.restoreCallingIdentity(identity); 5041 IoUtils.closeQuietly(outstream); 5042 } 5043 } 5044 getThumb(DatabaseHelper helper, SQLiteDatabase db, String path, long album_id, Uri albumart_uri)5045 private ParcelFileDescriptor getThumb(DatabaseHelper helper, SQLiteDatabase db, String path, 5046 long album_id, Uri albumart_uri) { 5047 ThumbData d = new ThumbData(); 5048 d.helper = helper; 5049 d.db = db; 5050 d.path = path; 5051 d.album_id = album_id; 5052 d.albumart_uri = albumart_uri; 5053 return makeThumbInternal(d); 5054 } 5055 makeThumbInternal(ThumbData d)5056 private ParcelFileDescriptor makeThumbInternal(ThumbData d) { 5057 byte[] compressed = getCompressedAlbumArt(getContext(), mExternalStoragePaths, d.path); 5058 5059 if (compressed == null) { 5060 return null; 5061 } 5062 5063 Bitmap bm = null; 5064 boolean need_to_recompress = true; 5065 5066 try { 5067 // get the size of the bitmap 5068 BitmapFactory.Options opts = new BitmapFactory.Options(); 5069 opts.inJustDecodeBounds = true; 5070 opts.inSampleSize = 1; 5071 BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts); 5072 5073 // request a reasonably sized output image 5074 final Resources r = getContext().getResources(); 5075 final int maximumThumbSize = r.getDimensionPixelSize(R.dimen.maximum_thumb_size); 5076 while (opts.outHeight > maximumThumbSize || opts.outWidth > maximumThumbSize) { 5077 opts.outHeight /= 2; 5078 opts.outWidth /= 2; 5079 opts.inSampleSize *= 2; 5080 } 5081 5082 if (opts.inSampleSize == 1) { 5083 // The original album art was of proper size, we won't have to 5084 // recompress the bitmap later. 5085 need_to_recompress = false; 5086 } else { 5087 // get the image for real now 5088 opts.inJustDecodeBounds = false; 5089 opts.inPreferredConfig = Bitmap.Config.RGB_565; 5090 bm = BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts); 5091 5092 if (bm != null && bm.getConfig() == null) { 5093 Bitmap nbm = bm.copy(Bitmap.Config.RGB_565, false); 5094 if (nbm != null && nbm != bm) { 5095 bm.recycle(); 5096 bm = nbm; 5097 } 5098 } 5099 } 5100 } catch (Exception e) { 5101 } 5102 5103 if (need_to_recompress && bm == null) { 5104 return null; 5105 } 5106 5107 if (d.albumart_uri == null) { 5108 // this one doesn't need to be saved (probably a song with an unknown album), 5109 // so stick it in a memory file and return that 5110 try { 5111 return ParcelFileDescriptor.fromData(compressed, "albumthumb"); 5112 } catch (IOException e) { 5113 } 5114 } else { 5115 // This one needs to actually be saved on the sd card. 5116 // This is wrapped in a transaction because there are various things 5117 // that could go wrong while generating the thumbnail, and we only want 5118 // to update the database when all steps succeeded. 5119 d.db.beginTransaction(); 5120 Uri out = null; 5121 ParcelFileDescriptor pfd = null; 5122 try { 5123 out = getAlbumArtOutputUri(d.helper, d.db, d.album_id, d.albumart_uri); 5124 5125 if (out != null) { 5126 writeAlbumArt(need_to_recompress, out, compressed, bm); 5127 getContext().getContentResolver().notifyChange(MEDIA_URI, null); 5128 pfd = openFileHelper(out, "r"); 5129 d.db.setTransactionSuccessful(); 5130 return pfd; 5131 } 5132 } catch (IOException ex) { 5133 // do nothing, just return null below 5134 } catch (UnsupportedOperationException ex) { 5135 // do nothing, just return null below 5136 } finally { 5137 d.db.endTransaction(); 5138 if (bm != null) { 5139 bm.recycle(); 5140 } 5141 if (pfd == null && out != null) { 5142 // Thumbnail was not written successfully, delete the entry that refers to it. 5143 // Note that this only does something if getAlbumArtOutputUri() reused an 5144 // existing entry from the database. If a new entry was created, it will 5145 // have been rolled back as part of backing out the transaction. 5146 5147 // Clear calling identity as we may be handling an IPC. 5148 final long identity = Binder.clearCallingIdentity(); 5149 try { 5150 getContext().getContentResolver().delete(out, null, null); 5151 } finally { 5152 Binder.restoreCallingIdentity(identity); 5153 } 5154 5155 } 5156 } 5157 } 5158 return null; 5159 } 5160 5161 /** 5162 * Look up the artist or album entry for the given name, creating that entry 5163 * if it does not already exists. 5164 * @param db The database 5165 * @param table The table to store the key/name pair in. 5166 * @param keyField The name of the key-column 5167 * @param nameField The name of the name-column 5168 * @param rawName The name that the calling app was trying to insert into the database 5169 * @param cacheName The string that will be inserted in to the cache 5170 * @param path The full path to the file being inserted in to the audio table 5171 * @param albumHash A hash to distinguish between different albums of the same name 5172 * @param artist The name of the artist, if known 5173 * @param cache The cache to add this entry to 5174 * @param srcuri The Uri that prompted the call to this method, used for determining whether this is 5175 * the internal or external database 5176 * @return The row ID for this artist/album, or -1 if the provided name was invalid 5177 */ getKeyIdForName(DatabaseHelper helper, SQLiteDatabase db, String table, String keyField, String nameField, String rawName, String cacheName, String path, int albumHash, String artist, HashMap<String, Long> cache, Uri srcuri)5178 private long getKeyIdForName(DatabaseHelper helper, SQLiteDatabase db, 5179 String table, String keyField, String nameField, 5180 String rawName, String cacheName, String path, int albumHash, 5181 String artist, HashMap<String, Long> cache, Uri srcuri) { 5182 long rowId; 5183 5184 if (rawName == null || rawName.length() == 0) { 5185 rawName = MediaStore.UNKNOWN_STRING; 5186 } 5187 String k = MediaStore.Audio.keyFor(rawName); 5188 5189 if (k == null) { 5190 // shouldn't happen, since we only get null keys for null inputs 5191 Log.e(TAG, "null key", new Exception()); 5192 return -1; 5193 } 5194 5195 boolean isAlbum = table.equals("albums"); 5196 boolean isUnknown = MediaStore.UNKNOWN_STRING.equals(rawName); 5197 5198 // To distinguish same-named albums, we append a hash. The hash is based 5199 // on the "album artist" tag if present, otherwise on the "compilation" tag 5200 // if present, otherwise on the path. 5201 // Ideally we would also take things like CDDB ID in to account, so 5202 // we can group files from the same album that aren't in the same 5203 // folder, but this is a quick and easy start that works immediately 5204 // without requiring support from the mp3, mp4 and Ogg meta data 5205 // readers, as long as the albums are in different folders. 5206 if (isAlbum) { 5207 k = k + albumHash; 5208 if (isUnknown) { 5209 k = k + artist; 5210 } 5211 } 5212 5213 String [] selargs = { k }; 5214 helper.mNumQueries++; 5215 Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null); 5216 5217 try { 5218 switch (c.getCount()) { 5219 case 0: { 5220 // insert new entry into table 5221 ContentValues otherValues = new ContentValues(); 5222 otherValues.put(keyField, k); 5223 otherValues.put(nameField, rawName); 5224 helper.mNumInserts++; 5225 rowId = db.insert(table, "duration", otherValues); 5226 if (path != null && isAlbum && ! isUnknown) { 5227 // We just inserted a new album. Now create an album art thumbnail for it. 5228 makeThumbAsync(helper, db, path, rowId); 5229 } 5230 if (rowId > 0) { 5231 String volume = srcuri.toString().substring(16, 24); // extract internal/external 5232 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 5233 getContext().getContentResolver().notifyChange(uri, null); 5234 } 5235 } 5236 break; 5237 case 1: { 5238 // Use the existing entry 5239 c.moveToFirst(); 5240 rowId = c.getLong(0); 5241 5242 // Determine whether the current rawName is better than what's 5243 // currently stored in the table, and update the table if it is. 5244 String currentFancyName = c.getString(2); 5245 String bestName = makeBestName(rawName, currentFancyName); 5246 if (!bestName.equals(currentFancyName)) { 5247 // update the table with the new name 5248 ContentValues newValues = new ContentValues(); 5249 newValues.put(nameField, bestName); 5250 helper.mNumUpdates++; 5251 db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null); 5252 String volume = srcuri.toString().substring(16, 24); // extract internal/external 5253 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 5254 getContext().getContentResolver().notifyChange(uri, null); 5255 } 5256 } 5257 break; 5258 default: 5259 // corrupt database 5260 Log.e(TAG, "Multiple entries in table " + table + " for key " + k); 5261 rowId = -1; 5262 break; 5263 } 5264 } finally { 5265 IoUtils.closeQuietly(c); 5266 } 5267 5268 if (cache != null && ! isUnknown) { 5269 cache.put(cacheName, rowId); 5270 } 5271 return rowId; 5272 } 5273 5274 /** 5275 * Returns the best string to use for display, given two names. 5276 * Note that this function does not necessarily return either one 5277 * of the provided names; it may decide to return a better alternative 5278 * (for example, specifying the inputs "Police" and "Police, The" will 5279 * return "The Police") 5280 * 5281 * The basic assumptions are: 5282 * - longer is better ("The police" is better than "Police") 5283 * - prefix is better ("The Police" is better than "Police, The") 5284 * - accents are better ("Motörhead" is better than "Motorhead") 5285 * 5286 * @param one The first of the two names to consider 5287 * @param two The last of the two names to consider 5288 * @return The actual name to use 5289 */ makeBestName(String one, String two)5290 String makeBestName(String one, String two) { 5291 String name; 5292 5293 // Longer names are usually better. 5294 if (one.length() > two.length()) { 5295 name = one; 5296 } else { 5297 // Names with accents are usually better, and conveniently sort later 5298 if (one.toLowerCase().compareTo(two.toLowerCase()) > 0) { 5299 name = one; 5300 } else { 5301 name = two; 5302 } 5303 } 5304 5305 // Prefixes are better than postfixes. 5306 if (name.endsWith(", the") || name.endsWith(",the") || 5307 name.endsWith(", an") || name.endsWith(",an") || 5308 name.endsWith(", a") || name.endsWith(",a")) { 5309 String fix = name.substring(1 + name.lastIndexOf(',')); 5310 name = fix.trim() + " " + name.substring(0, name.lastIndexOf(',')); 5311 } 5312 5313 // TODO: word-capitalize the resulting name 5314 return name; 5315 } 5316 5317 5318 /** 5319 * Looks up the database based on the given URI. 5320 * 5321 * @param uri The requested URI 5322 * @returns the database for the given URI 5323 */ getDatabaseForUri(Uri uri)5324 private DatabaseHelper getDatabaseForUri(Uri uri) { 5325 synchronized (mDatabases) { 5326 if (uri.getPathSegments().size() >= 1) { 5327 return mDatabases.get(uri.getPathSegments().get(0)); 5328 } 5329 } 5330 return null; 5331 } 5332 isMediaDatabaseName(String name)5333 static boolean isMediaDatabaseName(String name) { 5334 if (INTERNAL_DATABASE_NAME.equals(name)) { 5335 return true; 5336 } 5337 if (EXTERNAL_DATABASE_NAME.equals(name)) { 5338 return true; 5339 } 5340 if (name.startsWith("external-") && name.endsWith(".db")) { 5341 return true; 5342 } 5343 return false; 5344 } 5345 isInternalMediaDatabaseName(String name)5346 static boolean isInternalMediaDatabaseName(String name) { 5347 if (INTERNAL_DATABASE_NAME.equals(name)) { 5348 return true; 5349 } 5350 return false; 5351 } 5352 5353 /** 5354 * Attach the database for a volume (internal or external). 5355 * Does nothing if the volume is already attached, otherwise 5356 * checks the volume ID and sets up the corresponding database. 5357 * 5358 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}. 5359 * @return the content URI of the attached volume. 5360 */ attachVolume(String volume)5361 private Uri attachVolume(String volume) { 5362 if (Binder.getCallingPid() != Process.myPid()) { 5363 throw new SecurityException( 5364 "Opening and closing databases not allowed."); 5365 } 5366 5367 // Update paths to reflect currently mounted volumes 5368 updateStoragePaths(); 5369 5370 DatabaseHelper helper = null; 5371 synchronized (mDatabases) { 5372 helper = mDatabases.get(volume); 5373 if (helper != null) { 5374 if (EXTERNAL_VOLUME.equals(volume)) { 5375 ensureDefaultFolders(helper, helper.getWritableDatabase()); 5376 } 5377 return Uri.parse("content://media/" + volume); 5378 } 5379 5380 Context context = getContext(); 5381 if (INTERNAL_VOLUME.equals(volume)) { 5382 helper = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true, 5383 false, mObjectRemovedCallback); 5384 } else if (EXTERNAL_VOLUME.equals(volume)) { 5385 // Only extract FAT volume ID for primary public 5386 final VolumeInfo vol = mStorageManager.getPrimaryPhysicalVolume(); 5387 if (vol != null) { 5388 final StorageVolume actualVolume = mStorageManager.getPrimaryVolume(); 5389 final int volumeId = actualVolume.getFatVolumeId(); 5390 5391 // Must check for failure! 5392 // If the volume is not (yet) mounted, this will create a new 5393 // external-ffffffff.db database instead of the one we expect. Then, if 5394 // android.process.media is later killed and respawned, the real external 5395 // database will be attached, containing stale records, or worse, be empty. 5396 if (volumeId == -1) { 5397 String state = Environment.getExternalStorageState(); 5398 if (Environment.MEDIA_MOUNTED.equals(state) || 5399 Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 5400 // This may happen if external storage was _just_ mounted. It may also 5401 // happen if the volume ID is _actually_ 0xffffffff, in which case it 5402 // must be changed since FileUtils::getFatVolumeId doesn't allow for 5403 // that. It may also indicate that FileUtils::getFatVolumeId is broken 5404 // (missing ioctl), which is also impossible to disambiguate. 5405 Log.e(TAG, "Can't obtain external volume ID even though it's mounted."); 5406 } else { 5407 Log.i(TAG, "External volume is not (yet) mounted, cannot attach."); 5408 } 5409 5410 throw new IllegalArgumentException("Can't obtain external volume ID for " + 5411 volume + " volume."); 5412 } 5413 5414 // generate database name based on volume ID 5415 String dbName = "external-" + Integer.toHexString(volumeId) + ".db"; 5416 helper = new DatabaseHelper(context, dbName, false, 5417 false, mObjectRemovedCallback); 5418 mVolumeId = volumeId; 5419 } else { 5420 // external database name should be EXTERNAL_DATABASE_NAME 5421 // however earlier releases used the external-XXXXXXXX.db naming 5422 // for devices without removable storage, and in that case we need to convert 5423 // to this new convention 5424 File dbFile = context.getDatabasePath(EXTERNAL_DATABASE_NAME); 5425 if (!dbFile.exists() 5426 && android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 5427 // find the most recent external database and rename it to 5428 // EXTERNAL_DATABASE_NAME, and delete any other older 5429 // external database files 5430 File recentDbFile = null; 5431 for (String database : context.databaseList()) { 5432 if (database.startsWith("external-") && database.endsWith(".db")) { 5433 File file = context.getDatabasePath(database); 5434 if (recentDbFile == null) { 5435 recentDbFile = file; 5436 } else if (file.lastModified() > recentDbFile.lastModified()) { 5437 context.deleteDatabase(recentDbFile.getName()); 5438 recentDbFile = file; 5439 } else { 5440 context.deleteDatabase(file.getName()); 5441 } 5442 } 5443 } 5444 if (recentDbFile != null) { 5445 if (recentDbFile.renameTo(dbFile)) { 5446 Log.d(TAG, "renamed database " + recentDbFile.getName() + 5447 " to " + EXTERNAL_DATABASE_NAME); 5448 } else { 5449 Log.e(TAG, "Failed to rename database " + recentDbFile.getName() + 5450 " to " + EXTERNAL_DATABASE_NAME); 5451 // This shouldn't happen, but if it does, continue using 5452 // the file under its old name 5453 dbFile = recentDbFile; 5454 } 5455 } 5456 // else DatabaseHelper will create one named EXTERNAL_DATABASE_NAME 5457 } 5458 helper = new DatabaseHelper(context, dbFile.getName(), false, 5459 false, mObjectRemovedCallback); 5460 } 5461 } else { 5462 throw new IllegalArgumentException("There is no volume named " + volume); 5463 } 5464 5465 mDatabases.put(volume, helper); 5466 5467 if (!helper.mInternal) { 5468 // clean up stray album art files: delete every file not in the database 5469 File[] files = new File(mExternalStoragePaths[0], ALBUM_THUMB_FOLDER).listFiles(); 5470 HashSet<String> fileSet = new HashSet(); 5471 for (int i = 0; files != null && i < files.length; i++) { 5472 fileSet.add(files[i].getPath()); 5473 } 5474 5475 Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, 5476 new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null); 5477 try { 5478 while (cursor != null && cursor.moveToNext()) { 5479 fileSet.remove(cursor.getString(0)); 5480 } 5481 } finally { 5482 IoUtils.closeQuietly(cursor); 5483 } 5484 5485 Iterator<String> iterator = fileSet.iterator(); 5486 while (iterator.hasNext()) { 5487 String filename = iterator.next(); 5488 if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename); 5489 new File(filename).delete(); 5490 } 5491 } 5492 } 5493 5494 if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume); 5495 if (EXTERNAL_VOLUME.equals(volume)) { 5496 ensureDefaultFolders(helper, helper.getWritableDatabase()); 5497 } 5498 return Uri.parse("content://media/" + volume); 5499 } 5500 5501 /** 5502 * Detach the database for a volume (must be external). 5503 * Does nothing if the volume is already detached, otherwise 5504 * closes the database and sends a notification to listeners. 5505 * 5506 * @param uri The content URI of the volume, as returned by {@link #attachVolume} 5507 */ detachVolume(Uri uri)5508 private void detachVolume(Uri uri) { 5509 if (Binder.getCallingPid() != Process.myPid()) { 5510 throw new SecurityException( 5511 "Opening and closing databases not allowed."); 5512 } 5513 5514 // Update paths to reflect currently mounted volumes 5515 updateStoragePaths(); 5516 5517 String volume = uri.getPathSegments().get(0); 5518 if (INTERNAL_VOLUME.equals(volume)) { 5519 throw new UnsupportedOperationException( 5520 "Deleting the internal volume is not allowed"); 5521 } else if (!EXTERNAL_VOLUME.equals(volume)) { 5522 throw new IllegalArgumentException( 5523 "There is no volume named " + volume); 5524 } 5525 5526 synchronized (mDatabases) { 5527 DatabaseHelper database = mDatabases.get(volume); 5528 if (database == null) return; 5529 5530 try { 5531 // touch the database file to show it is most recently used 5532 File file = new File(database.getReadableDatabase().getPath()); 5533 file.setLastModified(System.currentTimeMillis()); 5534 } catch (Exception e) { 5535 Log.e(TAG, "Can't touch database file", e); 5536 } 5537 5538 mDatabases.remove(volume); 5539 database.close(); 5540 } 5541 5542 getContext().getContentResolver().notifyChange(uri, null); 5543 if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume); 5544 } 5545 5546 private static String TAG = "MediaProvider"; 5547 private static final boolean LOCAL_LOGV = false; 5548 5549 private static final String INTERNAL_DATABASE_NAME = "internal.db"; 5550 private static final String EXTERNAL_DATABASE_NAME = "external.db"; 5551 5552 // maximum number of cached external databases to keep 5553 private static final int MAX_EXTERNAL_DATABASES = 3; 5554 5555 // Delete databases that have not been used in two months 5556 // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60) 5557 private static final long OBSOLETE_DATABASE_DB = 5184000000L; 5558 5559 private HashMap<String, DatabaseHelper> mDatabases; 5560 5561 private Handler mThumbHandler; 5562 5563 // name of the volume currently being scanned by the media scanner (or null) 5564 private String mMediaScannerVolume; 5565 5566 // current FAT volume ID 5567 private int mVolumeId = -1; 5568 5569 static final String INTERNAL_VOLUME = "internal"; 5570 static final String EXTERNAL_VOLUME = "external"; 5571 static final String ALBUM_THUMB_FOLDER = "Android/data/com.android.providers.media/albumthumbs"; 5572 5573 // path for writing contents of in memory temp database 5574 private String mTempDatabasePath; 5575 5576 // WARNING: the values of IMAGES_MEDIA, AUDIO_MEDIA, and VIDEO_MEDIA and AUDIO_PLAYLISTS 5577 // are stored in the "files" table, so do not renumber them unless you also add 5578 // a corresponding database upgrade step for it. 5579 private static final int IMAGES_MEDIA = 1; 5580 private static final int IMAGES_MEDIA_ID = 2; 5581 private static final int IMAGES_THUMBNAILS = 3; 5582 private static final int IMAGES_THUMBNAILS_ID = 4; 5583 5584 private static final int AUDIO_MEDIA = 100; 5585 private static final int AUDIO_MEDIA_ID = 101; 5586 private static final int AUDIO_MEDIA_ID_GENRES = 102; 5587 private static final int AUDIO_MEDIA_ID_GENRES_ID = 103; 5588 private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104; 5589 private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105; 5590 private static final int AUDIO_GENRES = 106; 5591 private static final int AUDIO_GENRES_ID = 107; 5592 private static final int AUDIO_GENRES_ID_MEMBERS = 108; 5593 private static final int AUDIO_GENRES_ALL_MEMBERS = 109; 5594 private static final int AUDIO_PLAYLISTS = 110; 5595 private static final int AUDIO_PLAYLISTS_ID = 111; 5596 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 5597 private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113; 5598 private static final int AUDIO_ARTISTS = 114; 5599 private static final int AUDIO_ARTISTS_ID = 115; 5600 private static final int AUDIO_ALBUMS = 116; 5601 private static final int AUDIO_ALBUMS_ID = 117; 5602 private static final int AUDIO_ARTISTS_ID_ALBUMS = 118; 5603 private static final int AUDIO_ALBUMART = 119; 5604 private static final int AUDIO_ALBUMART_ID = 120; 5605 private static final int AUDIO_ALBUMART_FILE_ID = 121; 5606 5607 private static final int VIDEO_MEDIA = 200; 5608 private static final int VIDEO_MEDIA_ID = 201; 5609 private static final int VIDEO_THUMBNAILS = 202; 5610 private static final int VIDEO_THUMBNAILS_ID = 203; 5611 5612 private static final int VOLUMES = 300; 5613 private static final int VOLUMES_ID = 301; 5614 5615 private static final int AUDIO_SEARCH_LEGACY = 400; 5616 private static final int AUDIO_SEARCH_BASIC = 401; 5617 private static final int AUDIO_SEARCH_FANCY = 402; 5618 5619 private static final int MEDIA_SCANNER = 500; 5620 5621 private static final int FS_ID = 600; 5622 private static final int VERSION = 601; 5623 5624 private static final int FILES = 700; 5625 private static final int FILES_ID = 701; 5626 5627 // Used only by the MTP implementation 5628 private static final int MTP_OBJECTS = 702; 5629 private static final int MTP_OBJECTS_ID = 703; 5630 private static final int MTP_OBJECT_REFERENCES = 704; 5631 // UsbReceiver calls insert() and delete() with this URI to tell us 5632 // when MTP is connected and disconnected 5633 private static final int MTP_CONNECTED = 705; 5634 5635 private static final UriMatcher URI_MATCHER = 5636 new UriMatcher(UriMatcher.NO_MATCH); 5637 5638 private static final String[] ID_PROJECTION = new String[] { 5639 MediaStore.MediaColumns._ID 5640 }; 5641 5642 private static final String[] PATH_PROJECTION = new String[] { 5643 MediaStore.MediaColumns._ID, 5644 MediaStore.MediaColumns.DATA, 5645 }; 5646 5647 private static final String[] MIME_TYPE_PROJECTION = new String[] { 5648 MediaStore.MediaColumns._ID, // 0 5649 MediaStore.MediaColumns.MIME_TYPE, // 1 5650 }; 5651 5652 private static final String[] READY_FLAG_PROJECTION = new String[] { 5653 MediaStore.MediaColumns._ID, 5654 MediaStore.MediaColumns.DATA, 5655 Images.Media.MINI_THUMB_MAGIC 5656 }; 5657 5658 private static final String OBJECT_REFERENCES_QUERY = 5659 "SELECT " + Audio.Playlists.Members.AUDIO_ID + " FROM audio_playlists_map" 5660 + " WHERE " + Audio.Playlists.Members.PLAYLIST_ID + "=?" 5661 + " ORDER BY " + Audio.Playlists.Members.PLAY_ORDER; 5662 5663 static 5664 { 5665 URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA); 5666 URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID); 5667 URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS); 5668 URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID); 5669 5670 URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA); 5671 URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID); 5672 URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES); 5673 URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID); 5674 URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS); 5675 URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID); 5676 URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES); 5677 URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID); 5678 URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS); 5679 URI_MATCHER.addURI("media", "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS); 5680 URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS); 5681 URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID); 5682 URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS); 5683 URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID); 5684 URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS); 5685 URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID); 5686 URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS); 5687 URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS); 5688 URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID); 5689 URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART); 5690 URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID); 5691 URI_MATCHER.addURI("media", "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID); 5692 5693 URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA); 5694 URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID); 5695 URI_MATCHER.addURI("media", "*/video/thumbnails", VIDEO_THUMBNAILS); 5696 URI_MATCHER.addURI("media", "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID); 5697 5698 URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER); 5699 5700 URI_MATCHER.addURI("media", "*/fs_id", FS_ID); 5701 URI_MATCHER.addURI("media", "*/version", VERSION); 5702 5703 URI_MATCHER.addURI("media", "*/mtp_connected", MTP_CONNECTED); 5704 5705 URI_MATCHER.addURI("media", "*", VOLUMES_ID); 5706 URI_MATCHER.addURI("media", null, VOLUMES); 5707 5708 // Used by MTP implementation 5709 URI_MATCHER.addURI("media", "*/file", FILES); 5710 URI_MATCHER.addURI("media", "*/file/#", FILES_ID); 5711 URI_MATCHER.addURI("media", "*/object", MTP_OBJECTS); 5712 URI_MATCHER.addURI("media", "*/object/#", MTP_OBJECTS_ID); 5713 URI_MATCHER.addURI("media", "*/object/#/references", MTP_OBJECT_REFERENCES); 5714 5715 /** 5716 * @deprecated use the 'basic' or 'fancy' search Uris instead 5717 */ 5718 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY, 5719 AUDIO_SEARCH_LEGACY); 5720 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 5721 AUDIO_SEARCH_LEGACY); 5722 5723 // used for search suggestions 5724 URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY, 5725 AUDIO_SEARCH_BASIC); 5726 URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY + 5727 "/*", AUDIO_SEARCH_BASIC); 5728 5729 // used by the music app's search activity 5730 URI_MATCHER.addURI("media", "*/audio/search/fancy", AUDIO_SEARCH_FANCY); 5731 URI_MATCHER.addURI("media", "*/audio/search/fancy/*", AUDIO_SEARCH_FANCY); 5732 } 5733 getVolumeName(Uri uri)5734 private static String getVolumeName(Uri uri) { 5735 final List<String> segments = uri.getPathSegments(); 5736 if (segments != null && segments.size() > 0) { 5737 return segments.get(0); 5738 } else { 5739 return null; 5740 } 5741 } 5742 getCallingPackageOrSelf()5743 private String getCallingPackageOrSelf() { 5744 String callingPackage = getCallingPackage(); 5745 if (callingPackage == null) { 5746 callingPackage = getContext().getOpPackageName(); 5747 } 5748 return callingPackage; 5749 } 5750 enforceCallingOrSelfPermissionAndAppOps(String permission, String message)5751 private void enforceCallingOrSelfPermissionAndAppOps(String permission, String message) { 5752 getContext().enforceCallingOrSelfPermission(permission, message); 5753 5754 // Sure they have the permission, but has app-ops been revoked for 5755 // legacy apps? If so, they have no business being in here; we already 5756 // told them the volume was unmounted. 5757 final String opName = AppOpsManager.permissionToOp(permission); 5758 if (opName != null) { 5759 final String callingPackage = getCallingPackageOrSelf(); 5760 if (mAppOpsManager.noteProxyOp(opName, callingPackage) != AppOpsManager.MODE_ALLOWED) { 5761 throw new SecurityException( 5762 message + ": " + callingPackage + " is not allowed to " + permission); 5763 } 5764 } 5765 } 5766 5767 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)5768 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 5769 Collection<DatabaseHelper> foo = mDatabases.values(); 5770 for (DatabaseHelper dbh: foo) { 5771 writer.println(dump(dbh, true)); 5772 } 5773 writer.flush(); 5774 } 5775 dump(DatabaseHelper dbh, boolean dumpDbLog)5776 private String dump(DatabaseHelper dbh, boolean dumpDbLog) { 5777 StringBuilder s = new StringBuilder(); 5778 s.append(dbh.mName); 5779 s.append(": "); 5780 SQLiteDatabase db = dbh.getReadableDatabase(); 5781 if (db == null) { 5782 s.append("null"); 5783 } else { 5784 s.append("version " + db.getVersion() + ", "); 5785 Cursor c = db.query("files", new String[] {"count(*)"}, null, null, null, null, null); 5786 try { 5787 if (c != null && c.moveToFirst()) { 5788 int num = c.getInt(0); 5789 s.append(num + " rows, "); 5790 } else { 5791 s.append("couldn't get row count, "); 5792 } 5793 } finally { 5794 IoUtils.closeQuietly(c); 5795 } 5796 s.append(dbh.mNumInserts + " inserts, "); 5797 s.append(dbh.mNumUpdates + " updates, "); 5798 s.append(dbh.mNumDeletes + " deletes, "); 5799 s.append(dbh.mNumQueries + " queries, "); 5800 if (dbh.mScanStartTime != 0) { 5801 s.append("scan started " + DateUtils.formatDateTime(getContext(), 5802 dbh.mScanStartTime / 1000, 5803 DateUtils.FORMAT_SHOW_DATE 5804 | DateUtils.FORMAT_SHOW_TIME 5805 | DateUtils.FORMAT_ABBREV_ALL)); 5806 long now = dbh.mScanStopTime; 5807 if (now < dbh.mScanStartTime) { 5808 now = SystemClock.currentTimeMicro(); 5809 } 5810 s.append(" (" + DateUtils.formatElapsedTime( 5811 (now - dbh.mScanStartTime) / 1000000) + ")"); 5812 if (dbh.mScanStopTime < dbh.mScanStartTime) { 5813 if (mMediaScannerVolume != null && 5814 dbh.mName.startsWith(mMediaScannerVolume)) { 5815 s.append(" (ongoing)"); 5816 } else { 5817 s.append(" (scanning " + mMediaScannerVolume + ")"); 5818 } 5819 } 5820 } 5821 if (dumpDbLog) { 5822 c = db.query("log", new String[] {"time", "message"}, 5823 null, null, null, null, "rowid"); 5824 try { 5825 if (c != null) { 5826 while (c.moveToNext()) { 5827 String when = c.getString(0); 5828 String msg = c.getString(1); 5829 s.append("\n" + when + " : " + msg); 5830 } 5831 } 5832 } finally { 5833 IoUtils.closeQuietly(c); 5834 } 5835 } 5836 } 5837 return s.toString(); 5838 } 5839 } 5840