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