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