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