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