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