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