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