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