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