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