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