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