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