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