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