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