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