MediaProvider.java revision 677d5c9a353fd956c9cb981ca38d9fb351c0421d
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); 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 * Update the bucket_id and bucket_display_name columns for images and videos 1758 * @param db 1759 * @param tableName 1760 */ 1761 private static void updateBucketNames(SQLiteDatabase db) { 1762 // Rebuild the bucket_display_name column using the natural case rather than lower case. 1763 db.beginTransaction(); 1764 try { 1765 String[] columns = {BaseColumns._ID, MediaColumns.DATA}; 1766 // update only images and videos 1767 Cursor cursor = db.query("files", columns, "media_type=1 OR media_type=3", 1768 null, null, null, null); 1769 try { 1770 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 1771 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 1772 String [] rowId = new String[1]; 1773 while (cursor.moveToNext()) { 1774 String data = cursor.getString(dataColumnIndex); 1775 rowId[0] = String.valueOf(cursor.getInt(idColumnIndex)); 1776 if (data != null) { 1777 ContentValues values = new ContentValues(); 1778 computeBucketValues(data, values); 1779 db.update("files", values, "_id=?", rowId); 1780 } else { 1781 Log.w(TAG, "null data at id " + rowId); 1782 } 1783 } 1784 } finally { 1785 cursor.close(); 1786 } 1787 db.setTransactionSuccessful(); 1788 } finally { 1789 db.endTransaction(); 1790 } 1791 } 1792 1793 /** 1794 * Iterate through the rows of a table in a database, ensuring that the 1795 * display name column has a value. 1796 * @param db 1797 * @param tableName 1798 */ 1799 private static void updateDisplayName(SQLiteDatabase db, String tableName) { 1800 // Fill in default values for null displayName values 1801 db.beginTransaction(); 1802 try { 1803 String[] columns = {BaseColumns._ID, MediaColumns.DATA, MediaColumns.DISPLAY_NAME}; 1804 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 1805 try { 1806 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 1807 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 1808 final int displayNameIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME); 1809 ContentValues values = new ContentValues(); 1810 while (cursor.moveToNext()) { 1811 String displayName = cursor.getString(displayNameIndex); 1812 if (displayName == null) { 1813 String data = cursor.getString(dataColumnIndex); 1814 values.clear(); 1815 computeDisplayName(data, values); 1816 int rowId = cursor.getInt(idColumnIndex); 1817 db.update(tableName, values, "_id=" + rowId, null); 1818 } 1819 } 1820 } finally { 1821 cursor.close(); 1822 } 1823 db.setTransactionSuccessful(); 1824 } finally { 1825 db.endTransaction(); 1826 } 1827 } 1828 /** 1829 * @param data The input path 1830 * @param values the content values, where the bucked id name and bucket display name are updated. 1831 * 1832 */ 1833 1834 private static void computeBucketValues(String data, ContentValues values) { 1835 File parentFile = new File(data).getParentFile(); 1836 if (parentFile == null) { 1837 parentFile = new File("/"); 1838 } 1839 1840 // Lowercase the path for hashing. This avoids duplicate buckets if the 1841 // filepath case is changed externally. 1842 // Keep the original case for display. 1843 String path = parentFile.toString().toLowerCase(); 1844 String name = parentFile.getName(); 1845 1846 // Note: the BUCKET_ID and BUCKET_DISPLAY_NAME attributes are spelled the 1847 // same for both images and video. However, for backwards-compatibility reasons 1848 // there is no common base class. We use the ImageColumns version here 1849 values.put(ImageColumns.BUCKET_ID, path.hashCode()); 1850 values.put(ImageColumns.BUCKET_DISPLAY_NAME, name); 1851 } 1852 1853 /** 1854 * @param data The input path 1855 * @param values the content values, where the display name is updated. 1856 * 1857 */ 1858 private static void computeDisplayName(String data, ContentValues values) { 1859 String s = (data == null ? "" : data.toString()); 1860 int idx = s.lastIndexOf('/'); 1861 if (idx >= 0) { 1862 s = s.substring(idx + 1); 1863 } 1864 values.put("_display_name", s); 1865 } 1866 1867 /** 1868 * Copy taken time from date_modified if we lost the original value (e.g. after factory reset) 1869 * This works for both video and image tables. 1870 * 1871 * @param values the content values, where taken time is updated. 1872 */ 1873 private static void computeTakenTime(ContentValues values) { 1874 if (! values.containsKey(Images.Media.DATE_TAKEN)) { 1875 // This only happens when MediaScanner finds an image file that doesn't have any useful 1876 // reference to get this value. (e.g. GPSTimeStamp) 1877 Long lastModified = values.getAsLong(MediaColumns.DATE_MODIFIED); 1878 if (lastModified != null) { 1879 values.put(Images.Media.DATE_TAKEN, lastModified * 1000); 1880 } 1881 } 1882 } 1883 1884 /** 1885 * This method blocks until thumbnail is ready. 1886 * 1887 * @param thumbUri 1888 * @return 1889 */ 1890 private boolean waitForThumbnailReady(Uri origUri) { 1891 Cursor c = this.query(origUri, new String[] { ImageColumns._ID, ImageColumns.DATA, 1892 ImageColumns.MINI_THUMB_MAGIC}, null, null, null); 1893 if (c == null) return false; 1894 1895 boolean result = false; 1896 1897 if (c.moveToFirst()) { 1898 long id = c.getLong(0); 1899 String path = c.getString(1); 1900 long magic = c.getLong(2); 1901 1902 MediaThumbRequest req = requestMediaThumbnail(path, origUri, 1903 MediaThumbRequest.PRIORITY_HIGH, magic); 1904 if (req == null) { 1905 return false; 1906 } 1907 synchronized (req) { 1908 try { 1909 while (req.mState == MediaThumbRequest.State.WAIT) { 1910 req.wait(); 1911 } 1912 } catch (InterruptedException e) { 1913 Log.w(TAG, e); 1914 } 1915 if (req.mState == MediaThumbRequest.State.DONE) { 1916 result = true; 1917 } 1918 } 1919 } 1920 c.close(); 1921 1922 return result; 1923 } 1924 1925 private boolean matchThumbRequest(MediaThumbRequest req, int pid, long id, long gid, 1926 boolean isVideo) { 1927 boolean cancelAllOrigId = (id == -1); 1928 boolean cancelAllGroupId = (gid == -1); 1929 return (req.mCallingPid == pid) && 1930 (cancelAllGroupId || req.mGroupId == gid) && 1931 (cancelAllOrigId || req.mOrigId == id) && 1932 (req.mIsVideo == isVideo); 1933 } 1934 1935 private boolean queryThumbnail(SQLiteQueryBuilder qb, Uri uri, String table, 1936 String column, boolean hasThumbnailId) { 1937 qb.setTables(table); 1938 if (hasThumbnailId) { 1939 // For uri dispatched to this method, the 4th path segment is always 1940 // the thumbnail id. 1941 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 1942 // client already knows which thumbnail it wants, bypass it. 1943 return true; 1944 } 1945 String origId = uri.getQueryParameter("orig_id"); 1946 // We can't query ready_flag unless we know original id 1947 if (origId == null) { 1948 // this could be thumbnail query for other purpose, bypass it. 1949 return true; 1950 } 1951 1952 boolean needBlocking = "1".equals(uri.getQueryParameter("blocking")); 1953 boolean cancelRequest = "1".equals(uri.getQueryParameter("cancel")); 1954 Uri origUri = uri.buildUpon().encodedPath( 1955 uri.getPath().replaceFirst("thumbnails", "media")) 1956 .appendPath(origId).build(); 1957 1958 if (needBlocking && !waitForThumbnailReady(origUri)) { 1959 Log.w(TAG, "original media doesn't exist or it's canceled."); 1960 return false; 1961 } else if (cancelRequest) { 1962 String groupId = uri.getQueryParameter("group_id"); 1963 boolean isVideo = "video".equals(uri.getPathSegments().get(1)); 1964 int pid = Binder.getCallingPid(); 1965 long id = -1; 1966 long gid = -1; 1967 1968 try { 1969 id = Long.parseLong(origId); 1970 gid = Long.parseLong(groupId); 1971 } catch (NumberFormatException ex) { 1972 // invalid cancel request 1973 return false; 1974 } 1975 1976 synchronized (mMediaThumbQueue) { 1977 if (mCurrentThumbRequest != null && 1978 matchThumbRequest(mCurrentThumbRequest, pid, id, gid, isVideo)) { 1979 synchronized (mCurrentThumbRequest) { 1980 mCurrentThumbRequest.mState = MediaThumbRequest.State.CANCEL; 1981 mCurrentThumbRequest.notifyAll(); 1982 } 1983 } 1984 for (MediaThumbRequest mtq : mMediaThumbQueue) { 1985 if (matchThumbRequest(mtq, pid, id, gid, isVideo)) { 1986 synchronized (mtq) { 1987 mtq.mState = MediaThumbRequest.State.CANCEL; 1988 mtq.notifyAll(); 1989 } 1990 1991 mMediaThumbQueue.remove(mtq); 1992 } 1993 } 1994 } 1995 } 1996 1997 if (origId != null) { 1998 qb.appendWhere(column + " = " + origId); 1999 } 2000 return true; 2001 } 2002 @SuppressWarnings("fallthrough") 2003 @Override 2004 public Cursor query(Uri uri, String[] projectionIn, String selection, 2005 String[] selectionArgs, String sort) { 2006 int table = URI_MATCHER.match(uri); 2007 List<String> prependArgs = new ArrayList<String>(); 2008 2009 // Log.v(TAG, "query: uri="+uri+", selection="+selection); 2010 // handle MEDIA_SCANNER before calling getDatabaseForUri() 2011 if (table == MEDIA_SCANNER) { 2012 if (mMediaScannerVolume == null) { 2013 return null; 2014 } else { 2015 // create a cursor to return volume currently being scanned by the media scanner 2016 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME}); 2017 c.addRow(new String[] {mMediaScannerVolume}); 2018 return c; 2019 } 2020 } 2021 2022 // Used temporarily (until we have unique media IDs) to get an identifier 2023 // for the current sd card, so that the music app doesn't have to use the 2024 // non-public getFatVolumeId method 2025 if (table == FS_ID) { 2026 MatrixCursor c = new MatrixCursor(new String[] {"fsid"}); 2027 c.addRow(new Integer[] {mVolumeId}); 2028 return c; 2029 } 2030 2031 if (table == VERSION) { 2032 MatrixCursor c = new MatrixCursor(new String[] {"version"}); 2033 c.addRow(new Integer[] {getDatabaseVersion(getContext())}); 2034 return c; 2035 } 2036 2037 String groupBy = null; 2038 DatabaseHelper helper = getDatabaseForUri(uri); 2039 if (helper == null) { 2040 return null; 2041 } 2042 helper.mNumQueries++; 2043 SQLiteDatabase db = helper.getReadableDatabase(); 2044 if (db == null) return null; 2045 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2046 String limit = uri.getQueryParameter("limit"); 2047 String filter = uri.getQueryParameter("filter"); 2048 String [] keywords = null; 2049 if (filter != null) { 2050 filter = Uri.decode(filter).trim(); 2051 if (!TextUtils.isEmpty(filter)) { 2052 String [] searchWords = filter.split(" "); 2053 keywords = new String[searchWords.length]; 2054 Collator col = Collator.getInstance(); 2055 col.setStrength(Collator.PRIMARY); 2056 for (int i = 0; i < searchWords.length; i++) { 2057 String key = MediaStore.Audio.keyFor(searchWords[i]); 2058 key = key.replace("\\", "\\\\"); 2059 key = key.replace("%", "\\%"); 2060 key = key.replace("_", "\\_"); 2061 keywords[i] = key; 2062 } 2063 } 2064 } 2065 if (uri.getQueryParameter("distinct") != null) { 2066 qb.setDistinct(true); 2067 } 2068 2069 boolean hasThumbnailId = false; 2070 2071 switch (table) { 2072 case IMAGES_MEDIA: 2073 qb.setTables("images"); 2074 if (uri.getQueryParameter("distinct") != null) 2075 qb.setDistinct(true); 2076 2077 // set the project map so that data dir is prepended to _data. 2078 //qb.setProjectionMap(mImagesProjectionMap, true); 2079 break; 2080 2081 case IMAGES_MEDIA_ID: 2082 qb.setTables("images"); 2083 if (uri.getQueryParameter("distinct") != null) 2084 qb.setDistinct(true); 2085 2086 // set the project map so that data dir is prepended to _data. 2087 //qb.setProjectionMap(mImagesProjectionMap, true); 2088 qb.appendWhere("_id=?"); 2089 prependArgs.add(uri.getPathSegments().get(3)); 2090 break; 2091 2092 case IMAGES_THUMBNAILS_ID: 2093 hasThumbnailId = true; 2094 case IMAGES_THUMBNAILS: 2095 if (!queryThumbnail(qb, uri, "thumbnails", "image_id", hasThumbnailId)) { 2096 return null; 2097 } 2098 break; 2099 2100 case AUDIO_MEDIA: 2101 if (projectionIn != null && projectionIn.length == 1 && selectionArgs == null 2102 && (selection == null || selection.equalsIgnoreCase("is_music=1") 2103 || selection.equalsIgnoreCase("is_podcast=1") ) 2104 && projectionIn[0].equalsIgnoreCase("count(*)") 2105 && keywords != null) { 2106 //Log.i("@@@@", "taking fast path for counting songs"); 2107 qb.setTables("audio_meta"); 2108 } else { 2109 qb.setTables("audio"); 2110 for (int i = 0; keywords != null && i < keywords.length; i++) { 2111 if (i > 0) { 2112 qb.appendWhere(" AND "); 2113 } 2114 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2115 "||" + MediaStore.Audio.Media.ALBUM_KEY + 2116 "||" + MediaStore.Audio.Media.TITLE_KEY + " LIKE ? ESCAPE '\\'"); 2117 prependArgs.add("%" + keywords[i] + "%"); 2118 } 2119 } 2120 break; 2121 2122 case AUDIO_MEDIA_ID: 2123 qb.setTables("audio"); 2124 qb.appendWhere("_id=?"); 2125 prependArgs.add(uri.getPathSegments().get(3)); 2126 break; 2127 2128 case AUDIO_MEDIA_ID_GENRES: 2129 qb.setTables("audio_genres"); 2130 qb.appendWhere("_id IN (SELECT genre_id FROM " + 2131 "audio_genres_map WHERE audio_id=?)"); 2132 prependArgs.add(uri.getPathSegments().get(3)); 2133 break; 2134 2135 case AUDIO_MEDIA_ID_GENRES_ID: 2136 qb.setTables("audio_genres"); 2137 qb.appendWhere("_id=?"); 2138 prependArgs.add(uri.getPathSegments().get(5)); 2139 break; 2140 2141 case AUDIO_MEDIA_ID_PLAYLISTS: 2142 qb.setTables("audio_playlists"); 2143 qb.appendWhere("_id IN (SELECT playlist_id FROM " + 2144 "audio_playlists_map WHERE audio_id=?)"); 2145 prependArgs.add(uri.getPathSegments().get(3)); 2146 break; 2147 2148 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 2149 qb.setTables("audio_playlists"); 2150 qb.appendWhere("_id=?"); 2151 prependArgs.add(uri.getPathSegments().get(5)); 2152 break; 2153 2154 case AUDIO_GENRES: 2155 qb.setTables("audio_genres"); 2156 break; 2157 2158 case AUDIO_GENRES_ID: 2159 qb.setTables("audio_genres"); 2160 qb.appendWhere("_id=?"); 2161 prependArgs.add(uri.getPathSegments().get(3)); 2162 break; 2163 2164 case AUDIO_GENRES_ALL_MEMBERS: 2165 case AUDIO_GENRES_ID_MEMBERS: 2166 { 2167 // if simpleQuery is true, we can do a simpler query on just audio_genres_map 2168 // we can do this if we have no keywords and our projection includes just columns 2169 // from audio_genres_map 2170 boolean simpleQuery = (keywords == null && projectionIn != null 2171 && (selection == null || selection.equalsIgnoreCase("genre_id=?"))); 2172 if (projectionIn != null) { 2173 for (int i = 0; i < projectionIn.length; i++) { 2174 String p = projectionIn[i]; 2175 if (p.equals("_id")) { 2176 // note, this is different from playlist below, because 2177 // "_id" used to (wrongly) be the audio id in this query, not 2178 // the row id of the entry in the map, and we preserve this 2179 // behavior for backwards compatibility 2180 simpleQuery = false; 2181 } 2182 if (simpleQuery && !(p.equals("audio_id") || 2183 p.equals("genre_id"))) { 2184 simpleQuery = false; 2185 } 2186 } 2187 } 2188 if (simpleQuery) { 2189 qb.setTables("audio_genres_map_noid"); 2190 if (table == AUDIO_GENRES_ID_MEMBERS) { 2191 qb.appendWhere("genre_id=?"); 2192 prependArgs.add(uri.getPathSegments().get(3)); 2193 } 2194 } else { 2195 qb.setTables("audio_genres_map_noid, audio"); 2196 qb.appendWhere("audio._id = audio_id"); 2197 if (table == AUDIO_GENRES_ID_MEMBERS) { 2198 qb.appendWhere(" AND genre_id=?"); 2199 prependArgs.add(uri.getPathSegments().get(3)); 2200 } 2201 for (int i = 0; keywords != null && i < keywords.length; i++) { 2202 qb.appendWhere(" AND "); 2203 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2204 "||" + MediaStore.Audio.Media.ALBUM_KEY + 2205 "||" + MediaStore.Audio.Media.TITLE_KEY + 2206 " LIKE ? ESCAPE '\\'"); 2207 prependArgs.add("%" + keywords[i] + "%"); 2208 } 2209 } 2210 } 2211 break; 2212 2213 case AUDIO_PLAYLISTS: 2214 qb.setTables("audio_playlists"); 2215 break; 2216 2217 case AUDIO_PLAYLISTS_ID: 2218 qb.setTables("audio_playlists"); 2219 qb.appendWhere("_id=?"); 2220 prependArgs.add(uri.getPathSegments().get(3)); 2221 break; 2222 2223 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 2224 case AUDIO_PLAYLISTS_ID_MEMBERS: 2225 // if simpleQuery is true, we can do a simpler query on just audio_playlists_map 2226 // we can do this if we have no keywords and our projection includes just columns 2227 // from audio_playlists_map 2228 boolean simpleQuery = (keywords == null && projectionIn != null 2229 && (selection == null || selection.equalsIgnoreCase("playlist_id=?"))); 2230 if (projectionIn != null) { 2231 for (int i = 0; i < projectionIn.length; i++) { 2232 String p = projectionIn[i]; 2233 if (simpleQuery && !(p.equals("audio_id") || 2234 p.equals("playlist_id") || p.equals("play_order"))) { 2235 simpleQuery = false; 2236 } 2237 if (p.equals("_id")) { 2238 projectionIn[i] = "audio_playlists_map._id AS _id"; 2239 } 2240 } 2241 } 2242 if (simpleQuery) { 2243 qb.setTables("audio_playlists_map"); 2244 qb.appendWhere("playlist_id=?"); 2245 prependArgs.add(uri.getPathSegments().get(3)); 2246 } else { 2247 qb.setTables("audio_playlists_map, audio"); 2248 qb.appendWhere("audio._id = audio_id AND playlist_id=?"); 2249 prependArgs.add(uri.getPathSegments().get(3)); 2250 for (int i = 0; keywords != null && i < keywords.length; i++) { 2251 qb.appendWhere(" AND "); 2252 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2253 "||" + MediaStore.Audio.Media.ALBUM_KEY + 2254 "||" + MediaStore.Audio.Media.TITLE_KEY + 2255 " LIKE ? ESCAPE '\\'"); 2256 prependArgs.add("%" + keywords[i] + "%"); 2257 } 2258 } 2259 if (table == AUDIO_PLAYLISTS_ID_MEMBERS_ID) { 2260 qb.appendWhere(" AND audio_playlists_map._id=?"); 2261 prependArgs.add(uri.getPathSegments().get(5)); 2262 } 2263 break; 2264 2265 case VIDEO_MEDIA: 2266 qb.setTables("video"); 2267 break; 2268 case VIDEO_MEDIA_ID: 2269 qb.setTables("video"); 2270 qb.appendWhere("_id=?"); 2271 prependArgs.add(uri.getPathSegments().get(3)); 2272 break; 2273 2274 case VIDEO_THUMBNAILS_ID: 2275 hasThumbnailId = true; 2276 case VIDEO_THUMBNAILS: 2277 if (!queryThumbnail(qb, uri, "videothumbnails", "video_id", hasThumbnailId)) { 2278 return null; 2279 } 2280 break; 2281 2282 case AUDIO_ARTISTS: 2283 if (projectionIn != null && projectionIn.length == 1 && selectionArgs == null 2284 && (selection == null || selection.length() == 0) 2285 && projectionIn[0].equalsIgnoreCase("count(*)") 2286 && keywords != null) { 2287 //Log.i("@@@@", "taking fast path for counting artists"); 2288 qb.setTables("audio_meta"); 2289 projectionIn[0] = "count(distinct artist_id)"; 2290 qb.appendWhere("is_music=1"); 2291 } else { 2292 qb.setTables("artist_info"); 2293 for (int i = 0; keywords != null && i < keywords.length; i++) { 2294 if (i > 0) { 2295 qb.appendWhere(" AND "); 2296 } 2297 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2298 " LIKE ? ESCAPE '\\'"); 2299 prependArgs.add("%" + keywords[i] + "%"); 2300 } 2301 } 2302 break; 2303 2304 case AUDIO_ARTISTS_ID: 2305 qb.setTables("artist_info"); 2306 qb.appendWhere("_id=?"); 2307 prependArgs.add(uri.getPathSegments().get(3)); 2308 break; 2309 2310 case AUDIO_ARTISTS_ID_ALBUMS: 2311 String aid = uri.getPathSegments().get(3); 2312 qb.setTables("audio LEFT OUTER JOIN album_art ON" + 2313 " audio.album_id=album_art.album_id"); 2314 qb.appendWhere("is_music=1 AND audio.album_id IN (SELECT album_id FROM " + 2315 "artists_albums_map WHERE artist_id=?)"); 2316 prependArgs.add(aid); 2317 for (int i = 0; keywords != null && i < keywords.length; i++) { 2318 qb.appendWhere(" AND "); 2319 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2320 "||" + MediaStore.Audio.Media.ALBUM_KEY + 2321 " LIKE ? ESCAPE '\\'"); 2322 prependArgs.add("%" + keywords[i] + "%"); 2323 } 2324 groupBy = "audio.album_id"; 2325 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 2326 "count(CASE WHEN artist_id==" + aid + " THEN 'foo' ELSE NULL END) AS " + 2327 MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 2328 qb.setProjectionMap(sArtistAlbumsMap); 2329 break; 2330 2331 case AUDIO_ALBUMS: 2332 if (projectionIn != null && projectionIn.length == 1 && selectionArgs == null 2333 && (selection == null || selection.length() == 0) 2334 && projectionIn[0].equalsIgnoreCase("count(*)") 2335 && keywords != null) { 2336 //Log.i("@@@@", "taking fast path for counting albums"); 2337 qb.setTables("audio_meta"); 2338 projectionIn[0] = "count(distinct album_id)"; 2339 qb.appendWhere("is_music=1"); 2340 } else { 2341 qb.setTables("album_info"); 2342 for (int i = 0; keywords != null && i < keywords.length; i++) { 2343 if (i > 0) { 2344 qb.appendWhere(" AND "); 2345 } 2346 qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY + 2347 "||" + MediaStore.Audio.Media.ALBUM_KEY + 2348 " LIKE ? ESCAPE '\\'"); 2349 prependArgs.add("%" + keywords[i] + "%"); 2350 } 2351 } 2352 break; 2353 2354 case AUDIO_ALBUMS_ID: 2355 qb.setTables("album_info"); 2356 qb.appendWhere("_id=?"); 2357 prependArgs.add(uri.getPathSegments().get(3)); 2358 break; 2359 2360 case AUDIO_ALBUMART_ID: 2361 qb.setTables("album_art"); 2362 qb.appendWhere("album_id=?"); 2363 prependArgs.add(uri.getPathSegments().get(3)); 2364 break; 2365 2366 case AUDIO_SEARCH_LEGACY: 2367 Log.w(TAG, "Legacy media search Uri used. Please update your code."); 2368 // fall through 2369 case AUDIO_SEARCH_FANCY: 2370 case AUDIO_SEARCH_BASIC: 2371 return doAudioSearch(db, qb, uri, projectionIn, selection, 2372 combine(prependArgs, selectionArgs), sort, table, limit); 2373 2374 case FILES_ID: 2375 case MTP_OBJECTS_ID: 2376 qb.appendWhere("_id=?"); 2377 prependArgs.add(uri.getPathSegments().get(2)); 2378 // fall through 2379 case FILES: 2380 case MTP_OBJECTS: 2381 qb.setTables("files"); 2382 break; 2383 2384 case MTP_OBJECT_REFERENCES: 2385 int handle = Integer.parseInt(uri.getPathSegments().get(2)); 2386 return getObjectReferences(helper, db, handle); 2387 2388 default: 2389 throw new IllegalStateException("Unknown URL: " + uri.toString()); 2390 } 2391 2392 // Log.v(TAG, "query = "+ qb.buildQuery(projectionIn, selection, 2393 // combine(prependArgs, selectionArgs), groupBy, null, sort, limit)); 2394 Cursor c = qb.query(db, projectionIn, selection, 2395 combine(prependArgs, selectionArgs), groupBy, null, sort, limit); 2396 2397 if (c != null) { 2398 c.setNotificationUri(getContext().getContentResolver(), uri); 2399 } 2400 2401 return c; 2402 } 2403 2404 private String[] combine(List<String> prepend, String[] userArgs) { 2405 int presize = prepend.size(); 2406 if (presize == 0) { 2407 return userArgs; 2408 } 2409 2410 int usersize = (userArgs != null) ? userArgs.length : 0; 2411 String [] combined = new String[presize + usersize]; 2412 for (int i = 0; i < presize; i++) { 2413 combined[i] = prepend.get(i); 2414 } 2415 for (int i = 0; i < usersize; i++) { 2416 combined[presize + i] = userArgs[i]; 2417 } 2418 return combined; 2419 } 2420 2421 private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb, 2422 Uri uri, String[] projectionIn, String selection, 2423 String[] selectionArgs, String sort, int mode, 2424 String limit) { 2425 2426 String mSearchString = uri.getPath().endsWith("/") ? "" : uri.getLastPathSegment(); 2427 mSearchString = mSearchString.replaceAll(" ", " ").trim().toLowerCase(); 2428 2429 String [] searchWords = mSearchString.length() > 0 ? 2430 mSearchString.split(" ") : new String[0]; 2431 String [] wildcardWords = new String[searchWords.length]; 2432 Collator col = Collator.getInstance(); 2433 col.setStrength(Collator.PRIMARY); 2434 int len = searchWords.length; 2435 for (int i = 0; i < len; i++) { 2436 // Because we match on individual words here, we need to remove words 2437 // like 'a' and 'the' that aren't part of the keys. 2438 String key = MediaStore.Audio.keyFor(searchWords[i]); 2439 key = key.replace("\\", "\\\\"); 2440 key = key.replace("%", "\\%"); 2441 key = key.replace("_", "\\_"); 2442 wildcardWords[i] = 2443 (searchWords[i].equals("a") || searchWords[i].equals("an") || 2444 searchWords[i].equals("the")) ? "%" : "%" + key + "%"; 2445 } 2446 2447 String where = ""; 2448 for (int i = 0; i < searchWords.length; i++) { 2449 if (i == 0) { 2450 where = "match LIKE ? ESCAPE '\\'"; 2451 } else { 2452 where += " AND match LIKE ? ESCAPE '\\'"; 2453 } 2454 } 2455 2456 qb.setTables("search"); 2457 String [] cols; 2458 if (mode == AUDIO_SEARCH_FANCY) { 2459 cols = mSearchColsFancy; 2460 } else if (mode == AUDIO_SEARCH_BASIC) { 2461 cols = mSearchColsBasic; 2462 } else { 2463 cols = mSearchColsLegacy; 2464 } 2465 return qb.query(db, cols, where, wildcardWords, null, null, null, limit); 2466 } 2467 2468 @Override 2469 public String getType(Uri url) 2470 { 2471 switch (URI_MATCHER.match(url)) { 2472 case IMAGES_MEDIA_ID: 2473 case AUDIO_MEDIA_ID: 2474 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 2475 case VIDEO_MEDIA_ID: 2476 case FILES_ID: 2477 Cursor c = null; 2478 try { 2479 c = query(url, MIME_TYPE_PROJECTION, null, null, null); 2480 if (c != null && c.getCount() == 1) { 2481 c.moveToFirst(); 2482 String mimeType = c.getString(1); 2483 c.deactivate(); 2484 return mimeType; 2485 } 2486 } finally { 2487 if (c != null) { 2488 c.close(); 2489 } 2490 } 2491 break; 2492 2493 case IMAGES_MEDIA: 2494 case IMAGES_THUMBNAILS: 2495 return Images.Media.CONTENT_TYPE; 2496 case AUDIO_ALBUMART_ID: 2497 case IMAGES_THUMBNAILS_ID: 2498 return "image/jpeg"; 2499 2500 case AUDIO_MEDIA: 2501 case AUDIO_GENRES_ID_MEMBERS: 2502 case AUDIO_PLAYLISTS_ID_MEMBERS: 2503 return Audio.Media.CONTENT_TYPE; 2504 2505 case AUDIO_GENRES: 2506 case AUDIO_MEDIA_ID_GENRES: 2507 return Audio.Genres.CONTENT_TYPE; 2508 case AUDIO_GENRES_ID: 2509 case AUDIO_MEDIA_ID_GENRES_ID: 2510 return Audio.Genres.ENTRY_CONTENT_TYPE; 2511 case AUDIO_PLAYLISTS: 2512 case AUDIO_MEDIA_ID_PLAYLISTS: 2513 return Audio.Playlists.CONTENT_TYPE; 2514 case AUDIO_PLAYLISTS_ID: 2515 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 2516 return Audio.Playlists.ENTRY_CONTENT_TYPE; 2517 2518 case VIDEO_MEDIA: 2519 return Video.Media.CONTENT_TYPE; 2520 } 2521 throw new IllegalStateException("Unknown URL : " + url); 2522 } 2523 2524 /** 2525 * Ensures there is a file in the _data column of values, if one isn't 2526 * present a new file is created. 2527 * 2528 * @param initialValues the values passed to insert by the caller 2529 * @return the new values 2530 */ 2531 private ContentValues ensureFile(boolean internal, ContentValues initialValues, 2532 String preferredExtension, String directoryName) { 2533 ContentValues values; 2534 String file = initialValues.getAsString(MediaStore.MediaColumns.DATA); 2535 if (TextUtils.isEmpty(file)) { 2536 file = generateFileName(internal, preferredExtension, directoryName); 2537 values = new ContentValues(initialValues); 2538 values.put(MediaStore.MediaColumns.DATA, file); 2539 } else { 2540 values = initialValues; 2541 } 2542 2543 if (!ensureFileExists(file)) { 2544 throw new IllegalStateException("Unable to create new file: " + file); 2545 } 2546 return values; 2547 } 2548 2549 private void sendObjectAdded(long objectHandle) { 2550 synchronized (mMtpServiceConnection) { 2551 if (mMtpService != null) { 2552 try { 2553 mMtpService.sendObjectAdded((int)objectHandle); 2554 } catch (RemoteException e) { 2555 Log.e(TAG, "RemoteException in sendObjectAdded", e); 2556 mMtpService = null; 2557 } 2558 } 2559 } 2560 } 2561 2562 private void sendObjectRemoved(long objectHandle) { 2563 synchronized (mMtpServiceConnection) { 2564 if (mMtpService != null) { 2565 try { 2566 mMtpService.sendObjectRemoved((int)objectHandle); 2567 } catch (RemoteException e) { 2568 Log.e(TAG, "RemoteException in sendObjectRemoved", e); 2569 mMtpService = null; 2570 } 2571 } 2572 } 2573 } 2574 2575 @Override 2576 public int bulkInsert(Uri uri, ContentValues values[]) { 2577 int match = URI_MATCHER.match(uri); 2578 if (match == VOLUMES) { 2579 return super.bulkInsert(uri, values); 2580 } 2581 DatabaseHelper helper = getDatabaseForUri(uri); 2582 if (helper == null) { 2583 throw new UnsupportedOperationException( 2584 "Unknown URI: " + uri); 2585 } 2586 SQLiteDatabase db = helper.getWritableDatabase(); 2587 if (db == null) { 2588 throw new IllegalStateException("Couldn't open database for " + uri); 2589 } 2590 2591 if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) { 2592 return playlistBulkInsert(db, uri, values); 2593 } else if (match == MTP_OBJECT_REFERENCES) { 2594 int handle = Integer.parseInt(uri.getPathSegments().get(2)); 2595 return setObjectReferences(helper, db, handle, values); 2596 } 2597 2598 db.beginTransaction(); 2599 int numInserted = 0; 2600 try { 2601 int len = values.length; 2602 for (int i = 0; i < len; i++) { 2603 if (values[i] != null) { 2604 insertInternal(uri, match, values[i]); 2605 } 2606 } 2607 numInserted = len; 2608 db.setTransactionSuccessful(); 2609 } finally { 2610 db.endTransaction(); 2611 } 2612 getContext().getContentResolver().notifyChange(uri, null); 2613 return numInserted; 2614 } 2615 2616 @Override 2617 public Uri insert(Uri uri, ContentValues initialValues) { 2618 int match = URI_MATCHER.match(uri); 2619 Uri newUri = insertInternal(uri, match, initialValues); 2620 // do not signal notification for MTP objects. 2621 // we will signal instead after file transfer is successful. 2622 if (newUri != null && match != MTP_OBJECTS) { 2623 getContext().getContentResolver().notifyChange(uri, null); 2624 } 2625 return newUri; 2626 } 2627 2628 private int playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[]) { 2629 DatabaseUtils.InsertHelper helper = 2630 new DatabaseUtils.InsertHelper(db, "audio_playlists_map"); 2631 int audioidcolidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID); 2632 int playlistididx = helper.getColumnIndex(Audio.Playlists.Members.PLAYLIST_ID); 2633 int playorderidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 2634 long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 2635 2636 db.beginTransaction(); 2637 int numInserted = 0; 2638 try { 2639 int len = values.length; 2640 for (int i = 0; i < len; i++) { 2641 helper.prepareForInsert(); 2642 // getting the raw Object and converting it long ourselves saves 2643 // an allocation (the alternative is ContentValues.getAsLong, which 2644 // returns a Long object) 2645 long audioid = ((Number) values[i].get( 2646 MediaStore.Audio.Playlists.Members.AUDIO_ID)).longValue(); 2647 helper.bind(audioidcolidx, audioid); 2648 helper.bind(playlistididx, playlistId); 2649 // convert to int ourselves to save an allocation. 2650 int playorder = ((Number) values[i].get( 2651 MediaStore.Audio.Playlists.Members.PLAY_ORDER)).intValue(); 2652 helper.bind(playorderidx, playorder); 2653 helper.execute(); 2654 } 2655 numInserted = len; 2656 db.setTransactionSuccessful(); 2657 } finally { 2658 db.endTransaction(); 2659 helper.close(); 2660 } 2661 getContext().getContentResolver().notifyChange(uri, null); 2662 return numInserted; 2663 } 2664 2665 private long insertDirectory(DatabaseHelper helper, SQLiteDatabase db, String path) { 2666 if (LOCAL_LOGV) Log.v(TAG, "inserting directory " + path); 2667 ContentValues values = new ContentValues(); 2668 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 2669 values.put(FileColumns.DATA, path); 2670 values.put(FileColumns.PARENT, getParent(helper, db, path)); 2671 values.put(FileColumns.STORAGE_ID, getStorageId(path)); 2672 File file = new File(path); 2673 if (file.exists()) { 2674 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 2675 } 2676 helper.mNumInserts++; 2677 long rowId = db.insert("files", FileColumns.DATE_MODIFIED, values); 2678 sendObjectAdded(rowId); 2679 return rowId; 2680 } 2681 2682 private long getParent(DatabaseHelper helper, SQLiteDatabase db, String path) { 2683 int lastSlash = path.lastIndexOf('/'); 2684 if (lastSlash > 0) { 2685 String parentPath = path.substring(0, lastSlash); 2686 for (int i = 0; i < mExternalStoragePaths.length; i++) { 2687 if (parentPath.equals(mExternalStoragePaths[i])) { 2688 return 0; 2689 } 2690 } 2691 Long cid = mDirectoryCache.get(parentPath); 2692 if (cid != null) { 2693 if (LOCAL_LOGV) Log.v(TAG, "Returning cached entry for " + parentPath); 2694 return cid; 2695 } 2696 2697 // Use "LIKE" instead of "=" on case insensitive file systems so we do a 2698 // case insensitive match when looking for parent directory. 2699 // TODO: investigate whether a "nocase" constraint on the column and 2700 // using "=" would give the same result faster. 2701 String selection = (mCaseInsensitivePaths ? MediaStore.MediaColumns.DATA + " LIKE ?" 2702 // search only directories. 2703 + " AND format=" + MtpConstants.FORMAT_ASSOCIATION 2704 : MediaStore.MediaColumns.DATA + "=?"); 2705 String [] selargs = { parentPath }; 2706 helper.mNumQueries++; 2707 Cursor c = db.query("files", sIdOnlyColumn, selection, selargs, null, null, null); 2708 try { 2709 long id; 2710 if (c == null || c.getCount() == 0) { 2711 // parent isn't in the database - so add it 2712 id = insertDirectory(helper, db, parentPath); 2713 if (LOCAL_LOGV) Log.v(TAG, "Inserted " + parentPath); 2714 } else { 2715 c.moveToFirst(); 2716 id = c.getLong(0); 2717 if (LOCAL_LOGV) Log.v(TAG, "Queried " + parentPath); 2718 } 2719 mDirectoryCache.put(parentPath, id); 2720 return id; 2721 } finally { 2722 if (c != null) c.close(); 2723 } 2724 } else { 2725 return 0; 2726 } 2727 } 2728 2729 private int getStorageId(String path) { 2730 for (int i = 0; i < mExternalStoragePaths.length; i++) { 2731 String test = mExternalStoragePaths[i]; 2732 if (path.startsWith(test)) { 2733 int length = test.length(); 2734 if (path.length() == length || path.charAt(length) == '/') { 2735 return MtpStorage.getStorageId(i); 2736 } 2737 } 2738 } 2739 // default to primary storage 2740 return MtpStorage.getStorageId(0); 2741 } 2742 2743 private long insertFile(DatabaseHelper helper, Uri uri, ContentValues initialValues, int mediaType, 2744 boolean notify) { 2745 SQLiteDatabase db = helper.getWritableDatabase(); 2746 ContentValues values = null; 2747 2748 switch (mediaType) { 2749 case FileColumns.MEDIA_TYPE_IMAGE: { 2750 values = ensureFile(helper.mInternal, initialValues, ".jpg", "DCIM/Camera"); 2751 2752 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 2753 String data = values.getAsString(MediaColumns.DATA); 2754 if (! values.containsKey(MediaColumns.DISPLAY_NAME)) { 2755 computeDisplayName(data, values); 2756 } 2757 computeTakenTime(values); 2758 break; 2759 } 2760 2761 case FileColumns.MEDIA_TYPE_AUDIO: { 2762 // SQLite Views are read-only, so we need to deconstruct this 2763 // insert and do inserts into the underlying tables. 2764 // If doing this here turns out to be a performance bottleneck, 2765 // consider moving this to native code and using triggers on 2766 // the view. 2767 values = new ContentValues(initialValues); 2768 2769 String albumartist = values.getAsString(MediaStore.Audio.Media.ALBUM_ARTIST); 2770 String compilation = values.getAsString(MediaStore.Audio.Media.COMPILATION); 2771 values.remove(MediaStore.Audio.Media.COMPILATION); 2772 2773 // Insert the artist into the artist table and remove it from 2774 // the input values 2775 Object so = values.get("artist"); 2776 String s = (so == null ? "" : so.toString()); 2777 values.remove("artist"); 2778 long artistRowId; 2779 HashMap<String, Long> artistCache = helper.mArtistCache; 2780 String path = values.getAsString(MediaStore.MediaColumns.DATA); 2781 synchronized(artistCache) { 2782 Long temp = artistCache.get(s); 2783 if (temp == null) { 2784 artistRowId = getKeyIdForName(helper, db, 2785 "artists", "artist_key", "artist", 2786 s, s, path, 0, null, artistCache, uri); 2787 } else { 2788 artistRowId = temp.longValue(); 2789 } 2790 } 2791 String artist = s; 2792 2793 // Do the same for the album field 2794 so = values.get("album"); 2795 s = (so == null ? "" : so.toString()); 2796 values.remove("album"); 2797 long albumRowId; 2798 HashMap<String, Long> albumCache = helper.mAlbumCache; 2799 synchronized(albumCache) { 2800 int albumhash = 0; 2801 if (albumartist != null) { 2802 albumhash = albumartist.hashCode(); 2803 } else if (compilation != null && compilation.equals("1")) { 2804 // nothing to do, hash already set 2805 } else { 2806 albumhash = path.substring(0, path.lastIndexOf('/')).hashCode(); 2807 } 2808 String cacheName = s + albumhash; 2809 Long temp = albumCache.get(cacheName); 2810 if (temp == null) { 2811 albumRowId = getKeyIdForName(helper, db, 2812 "albums", "album_key", "album", 2813 s, cacheName, path, albumhash, artist, albumCache, uri); 2814 } else { 2815 albumRowId = temp; 2816 } 2817 } 2818 2819 values.put("artist_id", Integer.toString((int)artistRowId)); 2820 values.put("album_id", Integer.toString((int)albumRowId)); 2821 so = values.getAsString("title"); 2822 s = (so == null ? "" : so.toString()); 2823 values.put("title_key", MediaStore.Audio.keyFor(s)); 2824 // do a final trim of the title, in case it started with the special 2825 // "sort first" character (ascii \001) 2826 values.remove("title"); 2827 values.put("title", s.trim()); 2828 2829 computeDisplayName(values.getAsString(MediaStore.MediaColumns.DATA), values); 2830 break; 2831 } 2832 2833 case FileColumns.MEDIA_TYPE_VIDEO: { 2834 values = ensureFile(helper.mInternal, initialValues, ".3gp", "video"); 2835 String data = values.getAsString(MediaStore.MediaColumns.DATA); 2836 computeDisplayName(data, values); 2837 computeTakenTime(values); 2838 break; 2839 } 2840 } 2841 2842 if (values == null) { 2843 values = new ContentValues(initialValues); 2844 } 2845 // compute bucket_id and bucket_display_name for all files 2846 String path = values.getAsString(MediaStore.MediaColumns.DATA); 2847 if (path != null) { 2848 computeBucketValues(path, values); 2849 } 2850 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 2851 2852 long rowId = 0; 2853 Integer i = values.getAsInteger( 2854 MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID); 2855 if (i != null) { 2856 rowId = i.intValue(); 2857 values = new ContentValues(values); 2858 values.remove(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID); 2859 } 2860 2861 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 2862 if (title == null && path != null) { 2863 title = MediaFile.getFileTitle(path); 2864 } 2865 values.put(FileColumns.TITLE, title); 2866 2867 String mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE); 2868 Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 2869 int format = (formatObject == null ? 0 : formatObject.intValue()); 2870 if (format == 0) { 2871 if (TextUtils.isEmpty(path)) { 2872 // special case device created playlists 2873 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 2874 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST); 2875 // create a file path for the benefit of MTP 2876 path = mExternalStoragePaths[0] 2877 + "/Playlists/" + values.getAsString(Audio.Playlists.NAME); 2878 values.put(MediaStore.MediaColumns.DATA, path); 2879 values.put(FileColumns.PARENT, getParent(helper, db, path)); 2880 } else { 2881 Log.e(TAG, "path is empty in insertFile()"); 2882 } 2883 } else { 2884 format = MediaFile.getFormatCode(path, mimeType); 2885 } 2886 } 2887 if (format != 0) { 2888 values.put(FileColumns.FORMAT, format); 2889 if (mimeType == null) { 2890 mimeType = MediaFile.getMimeTypeForFormatCode(format); 2891 } 2892 } 2893 2894 if (mimeType == null && path != null) { 2895 mimeType = MediaFile.getMimeTypeForFile(path); 2896 } 2897 if (mimeType != null) { 2898 values.put(FileColumns.MIME_TYPE, mimeType); 2899 2900 if (mediaType == FileColumns.MEDIA_TYPE_NONE && !MediaScanner.isNoMediaPath(path)) { 2901 int fileType = MediaFile.getFileTypeForMimeType(mimeType); 2902 if (MediaFile.isAudioFileType(fileType)) { 2903 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 2904 } else if (MediaFile.isVideoFileType(fileType)) { 2905 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 2906 } else if (MediaFile.isImageFileType(fileType)) { 2907 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 2908 } else if (MediaFile.isPlayListFileType(fileType)) { 2909 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 2910 } 2911 } 2912 } 2913 values.put(FileColumns.MEDIA_TYPE, mediaType); 2914 2915 if (rowId == 0) { 2916 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 2917 String name = values.getAsString(Audio.Playlists.NAME); 2918 if (name == null && path == null) { 2919 // MediaScanner will compute the name from the path if we have one 2920 throw new IllegalArgumentException( 2921 "no name was provided when inserting abstract playlist"); 2922 } 2923 } else { 2924 if (path == null) { 2925 // path might be null for playlists created on the device 2926 // or transfered via MTP 2927 throw new IllegalArgumentException( 2928 "no path was provided when inserting new file"); 2929 } 2930 } 2931 2932 // make sure modification date and size are set 2933 if (path != null) { 2934 File file = new File(path); 2935 if (file.exists()) { 2936 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 2937 values.put(FileColumns.SIZE, file.length()); 2938 } 2939 } 2940 2941 Long parent = values.getAsLong(FileColumns.PARENT); 2942 if (parent == null) { 2943 if (path != null) { 2944 long parentId = getParent(helper, db, path); 2945 values.put(FileColumns.PARENT, parentId); 2946 } 2947 } 2948 Integer storage = values.getAsInteger(FileColumns.STORAGE_ID); 2949 if (storage == null) { 2950 int storageId = getStorageId(path); 2951 values.put(FileColumns.STORAGE_ID, storageId); 2952 } 2953 2954 helper.mNumInserts++; 2955 rowId = db.insert("files", FileColumns.DATE_MODIFIED, values); 2956 if (LOCAL_LOGV) Log.v(TAG, "insertFile: values=" + values + " returned: " + rowId); 2957 2958 if (rowId != -1 && notify) { 2959 sendObjectAdded(rowId); 2960 } 2961 } else { 2962 helper.mNumUpdates++; 2963 db.update("files", values, FileColumns._ID + "=?", 2964 new String[] { Long.toString(rowId) }); 2965 } 2966 if (format == MtpConstants.FORMAT_ASSOCIATION) { 2967 mDirectoryCache.put(path, rowId); 2968 } 2969 2970 return rowId; 2971 } 2972 2973 private Cursor getObjectReferences(DatabaseHelper helper, SQLiteDatabase db, int handle) { 2974 helper.mNumQueries++; 2975 Cursor c = db.query("files", sMediaTableColumns, "_id=?", 2976 new String[] { Integer.toString(handle) }, 2977 null, null, null); 2978 try { 2979 if (c != null && c.moveToNext()) { 2980 long playlistId = c.getLong(0); 2981 int mediaType = c.getInt(1); 2982 if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) { 2983 // we only support object references for playlist objects 2984 return null; 2985 } 2986 helper.mNumQueries++; 2987 return db.rawQuery(OBJECT_REFERENCES_QUERY, 2988 new String[] { Long.toString(playlistId) } ); 2989 } 2990 } finally { 2991 if (c != null) { 2992 c.close(); 2993 } 2994 } 2995 return null; 2996 } 2997 2998 private int setObjectReferences(DatabaseHelper helper, SQLiteDatabase db, 2999 int handle, ContentValues values[]) { 3000 // first look up the media table and media ID for the object 3001 long playlistId = 0; 3002 helper.mNumQueries++; 3003 Cursor c = db.query("files", sMediaTableColumns, "_id=?", 3004 new String[] { Integer.toString(handle) }, 3005 null, null, null); 3006 try { 3007 if (c != null && c.moveToNext()) { 3008 int mediaType = c.getInt(1); 3009 if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) { 3010 // we only support object references for playlist objects 3011 return 0; 3012 } 3013 playlistId = c.getLong(0); 3014 } 3015 } finally { 3016 if (c != null) { 3017 c.close(); 3018 } 3019 } 3020 if (playlistId == 0) { 3021 return 0; 3022 } 3023 3024 // next delete any existing entries 3025 helper.mNumDeletes++; 3026 db.delete("audio_playlists_map", "playlist_id=?", 3027 new String[] { Long.toString(playlistId) }); 3028 3029 // finally add the new entries 3030 int count = values.length; 3031 int added = 0; 3032 ContentValues[] valuesList = new ContentValues[count]; 3033 for (int i = 0; i < count; i++) { 3034 // convert object ID to audio ID 3035 long audioId = 0; 3036 long objectId = values[i].getAsLong(MediaStore.MediaColumns._ID); 3037 helper.mNumQueries++; 3038 c = db.query("files", sMediaTableColumns, "_id=?", 3039 new String[] { Long.toString(objectId) }, 3040 null, null, null); 3041 try { 3042 if (c != null && c.moveToNext()) { 3043 int mediaType = c.getInt(1); 3044 if (mediaType != FileColumns.MEDIA_TYPE_AUDIO) { 3045 // we only allow audio files in playlists, so skip 3046 continue; 3047 } 3048 audioId = c.getLong(0); 3049 } 3050 } finally { 3051 if (c != null) { 3052 c.close(); 3053 } 3054 } 3055 if (audioId != 0) { 3056 ContentValues v = new ContentValues(); 3057 v.put(MediaStore.Audio.Playlists.Members.PLAYLIST_ID, playlistId); 3058 v.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId); 3059 v.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, added); 3060 valuesList[added++] = v; 3061 } 3062 } 3063 if (added < count) { 3064 // we weren't able to find everything on the list, so lets resize the array 3065 // and pass what we have. 3066 ContentValues[] newValues = new ContentValues[added]; 3067 System.arraycopy(valuesList, 0, newValues, 0, added); 3068 valuesList = newValues; 3069 } 3070 return playlistBulkInsert(db, 3071 Audio.Playlists.Members.getContentUri(EXTERNAL_VOLUME, playlistId), 3072 valuesList); 3073 } 3074 3075 private static final String[] GENRE_LOOKUP_PROJECTION = new String[] { 3076 Audio.Genres._ID, // 0 3077 Audio.Genres.NAME, // 1 3078 }; 3079 3080 private void updateGenre(long rowId, String genre) { 3081 Uri uri = null; 3082 Cursor cursor = null; 3083 Uri genresUri = MediaStore.Audio.Genres.getContentUri("external"); 3084 try { 3085 // see if the genre already exists 3086 cursor = query(genresUri, GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", 3087 new String[] { genre }, null); 3088 if (cursor == null || cursor.getCount() == 0) { 3089 // genre does not exist, so create the genre in the genre table 3090 ContentValues values = new ContentValues(); 3091 values.put(MediaStore.Audio.Genres.NAME, genre); 3092 uri = insert(genresUri, values); 3093 } else { 3094 // genre already exists, so compute its Uri 3095 cursor.moveToNext(); 3096 uri = ContentUris.withAppendedId(genresUri, cursor.getLong(0)); 3097 } 3098 if (uri != null) { 3099 uri = Uri.withAppendedPath(uri, MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY); 3100 } 3101 } finally { 3102 // release the cursor if it exists 3103 if (cursor != null) { 3104 cursor.close(); 3105 } 3106 } 3107 3108 if (uri != null) { 3109 // add entry to audio_genre_map 3110 ContentValues values = new ContentValues(); 3111 values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); 3112 insert(uri, values); 3113 } 3114 } 3115 3116 private Uri insertInternal(Uri uri, int match, ContentValues initialValues) { 3117 long rowId; 3118 3119 if (LOCAL_LOGV) Log.v(TAG, "insertInternal: "+uri+", initValues="+initialValues); 3120 // handle MEDIA_SCANNER before calling getDatabaseForUri() 3121 if (match == MEDIA_SCANNER) { 3122 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 3123 DatabaseHelper database = getDatabaseForUri( 3124 Uri.parse("content://media/" + mMediaScannerVolume + "/audio")); 3125 if (database == null) { 3126 Log.w(TAG, "no database for scanned volume " + mMediaScannerVolume); 3127 } else { 3128 database.mScanStartTime = SystemClock.currentTimeMicro(); 3129 } 3130 return MediaStore.getMediaScannerUri(); 3131 } 3132 3133 String genre = null; 3134 String path = null; 3135 if (initialValues != null) { 3136 genre = initialValues.getAsString(Audio.AudioColumns.GENRE); 3137 initialValues.remove(Audio.AudioColumns.GENRE); 3138 path = initialValues.getAsString(MediaStore.MediaColumns.DATA); 3139 } 3140 3141 3142 Uri newUri = null; 3143 DatabaseHelper helper = getDatabaseForUri(uri); 3144 if (helper == null && match != VOLUMES && match != MTP_CONNECTED) { 3145 throw new UnsupportedOperationException( 3146 "Unknown URI: " + uri); 3147 } 3148 3149 SQLiteDatabase db = ((match == VOLUMES || match == MTP_CONNECTED) ? null 3150 : helper.getWritableDatabase()); 3151 3152 switch (match) { 3153 case IMAGES_MEDIA: { 3154 rowId = insertFile(helper, uri, initialValues, FileColumns.MEDIA_TYPE_IMAGE, true); 3155 if (rowId > 0) { 3156 newUri = ContentUris.withAppendedId( 3157 Images.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 3158 } 3159 break; 3160 } 3161 3162 // This will be triggered by requestMediaThumbnail (see getThumbnailUri) 3163 case IMAGES_THUMBNAILS: { 3164 ContentValues values = ensureFile(helper.mInternal, initialValues, ".jpg", 3165 "DCIM/.thumbnails"); 3166 helper.mNumInserts++; 3167 rowId = db.insert("thumbnails", "name", values); 3168 if (rowId > 0) { 3169 newUri = ContentUris.withAppendedId(Images.Thumbnails. 3170 getContentUri(uri.getPathSegments().get(0)), rowId); 3171 } 3172 break; 3173 } 3174 3175 // This is currently only used by MICRO_KIND video thumbnail (see getThumbnailUri) 3176 case VIDEO_THUMBNAILS: { 3177 ContentValues values = ensureFile(helper.mInternal, initialValues, ".jpg", 3178 "DCIM/.thumbnails"); 3179 helper.mNumInserts++; 3180 rowId = db.insert("videothumbnails", "name", values); 3181 if (rowId > 0) { 3182 newUri = ContentUris.withAppendedId(Video.Thumbnails. 3183 getContentUri(uri.getPathSegments().get(0)), rowId); 3184 } 3185 break; 3186 } 3187 3188 case AUDIO_MEDIA: { 3189 rowId = insertFile(helper, uri, initialValues, FileColumns.MEDIA_TYPE_AUDIO, true); 3190 if (rowId > 0) { 3191 newUri = ContentUris.withAppendedId(Audio.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 3192 if (genre != null) { 3193 updateGenre(rowId, genre); 3194 } 3195 } 3196 break; 3197 } 3198 3199 case AUDIO_MEDIA_ID_GENRES: { 3200 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 3201 ContentValues values = new ContentValues(initialValues); 3202 values.put(Audio.Genres.Members.AUDIO_ID, audioId); 3203 helper.mNumInserts++; 3204 rowId = db.insert("audio_genres_map", "genre_id", values); 3205 if (rowId > 0) { 3206 newUri = ContentUris.withAppendedId(uri, rowId); 3207 } 3208 break; 3209 } 3210 3211 case AUDIO_MEDIA_ID_PLAYLISTS: { 3212 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 3213 ContentValues values = new ContentValues(initialValues); 3214 values.put(Audio.Playlists.Members.AUDIO_ID, audioId); 3215 helper.mNumInserts++; 3216 rowId = db.insert("audio_playlists_map", "playlist_id", 3217 values); 3218 if (rowId > 0) { 3219 newUri = ContentUris.withAppendedId(uri, rowId); 3220 } 3221 break; 3222 } 3223 3224 case AUDIO_GENRES: { 3225 helper.mNumInserts++; 3226 rowId = db.insert("audio_genres", "audio_id", initialValues); 3227 if (rowId > 0) { 3228 newUri = ContentUris.withAppendedId(Audio.Genres.getContentUri(uri.getPathSegments().get(0)), rowId); 3229 } 3230 break; 3231 } 3232 3233 case AUDIO_GENRES_ID_MEMBERS: { 3234 Long genreId = Long.parseLong(uri.getPathSegments().get(3)); 3235 ContentValues values = new ContentValues(initialValues); 3236 values.put(Audio.Genres.Members.GENRE_ID, genreId); 3237 helper.mNumInserts++; 3238 rowId = db.insert("audio_genres_map", "genre_id", values); 3239 if (rowId > 0) { 3240 newUri = ContentUris.withAppendedId(uri, rowId); 3241 } 3242 break; 3243 } 3244 3245 case AUDIO_PLAYLISTS: { 3246 ContentValues values = new ContentValues(initialValues); 3247 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 3248 rowId = insertFile(helper, uri, values, FileColumns.MEDIA_TYPE_PLAYLIST, true); 3249 if (rowId > 0) { 3250 newUri = ContentUris.withAppendedId(Audio.Playlists.getContentUri(uri.getPathSegments().get(0)), rowId); 3251 } 3252 break; 3253 } 3254 3255 case AUDIO_PLAYLISTS_ID: 3256 case AUDIO_PLAYLISTS_ID_MEMBERS: { 3257 Long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 3258 ContentValues values = new ContentValues(initialValues); 3259 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId); 3260 helper.mNumInserts++; 3261 rowId = db.insert("audio_playlists_map", "playlist_id", values); 3262 if (rowId > 0) { 3263 newUri = ContentUris.withAppendedId(uri, rowId); 3264 } 3265 break; 3266 } 3267 3268 case VIDEO_MEDIA: { 3269 rowId = insertFile(helper, uri, initialValues, FileColumns.MEDIA_TYPE_VIDEO, true); 3270 if (rowId > 0) { 3271 newUri = ContentUris.withAppendedId(Video.Media.getContentUri( 3272 uri.getPathSegments().get(0)), rowId); 3273 } 3274 break; 3275 } 3276 3277 case AUDIO_ALBUMART: { 3278 if (helper.mInternal) { 3279 throw new UnsupportedOperationException("no internal album art allowed"); 3280 } 3281 ContentValues values = null; 3282 try { 3283 values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 3284 } catch (IllegalStateException ex) { 3285 // probably no more room to store albumthumbs 3286 values = initialValues; 3287 } 3288 helper.mNumInserts++; 3289 rowId = db.insert("album_art", MediaStore.MediaColumns.DATA, values); 3290 if (rowId > 0) { 3291 newUri = ContentUris.withAppendedId(uri, rowId); 3292 } 3293 break; 3294 } 3295 3296 case VOLUMES: 3297 { 3298 String name = initialValues.getAsString("name"); 3299 Uri attachedVolume = attachVolume(name); 3300 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) { 3301 DatabaseHelper dbhelper = getDatabaseForUri(attachedVolume); 3302 if (dbhelper == null) { 3303 Log.e(TAG, "no database for attached volume " + attachedVolume); 3304 } else { 3305 dbhelper.mScanStartTime = SystemClock.currentTimeMicro(); 3306 } 3307 } 3308 return attachedVolume; 3309 } 3310 3311 case MTP_CONNECTED: 3312 synchronized (mMtpServiceConnection) { 3313 if (mMtpService == null) { 3314 Context context = getContext(); 3315 // MTP is connected, so grab a connection to MtpService 3316 context.bindService(new Intent(context, MtpService.class), 3317 mMtpServiceConnection, Context.BIND_AUTO_CREATE); 3318 } 3319 } 3320 break; 3321 3322 case FILES: 3323 rowId = insertFile(helper, uri, initialValues, 3324 FileColumns.MEDIA_TYPE_NONE, true); 3325 if (rowId > 0) { 3326 newUri = Files.getContentUri(uri.getPathSegments().get(0), rowId); 3327 } 3328 break; 3329 3330 case MTP_OBJECTS: 3331 // don't send a notification if the insert originated from MTP 3332 rowId = insertFile(helper, uri, initialValues, 3333 FileColumns.MEDIA_TYPE_NONE, false); 3334 if (rowId > 0) { 3335 newUri = Files.getMtpObjectsUri(uri.getPathSegments().get(0), rowId); 3336 } 3337 break; 3338 3339 default: 3340 throw new UnsupportedOperationException("Invalid URI " + uri); 3341 } 3342 3343 if (path != null && path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 3344 // need to set the media_type of all the files below this folder to 0 3345 processNewNoMediaPath(helper, db, path); 3346 } 3347 return newUri; 3348 } 3349 3350 /* 3351 * Sets the media type of all files below the newly added .nomedia file or 3352 * hidden folder to 0, so the entries no longer appear in e.g. the audio and 3353 * images views. 3354 * 3355 * @param path The path to the new .nomedia file or hidden directory 3356 */ 3357 private void processNewNoMediaPath(DatabaseHelper helper, SQLiteDatabase db, String path) { 3358 File nomedia = new File(path); 3359 if (nomedia.exists()) { 3360 // only do this if the file actually exists, so we don't get tricked 3361 // by someone just inserting a .nomedia entry into the database 3362 String hiddenroot = nomedia.isDirectory() ? path : nomedia.getParent(); 3363 ContentValues mediatype = new ContentValues(); 3364 mediatype.put("media_type", 0); 3365 int numrows = db.update("files", mediatype, "_data LIKE ?", new String[] { hiddenroot + "/%"}); 3366 helper.mNumUpdates += numrows; 3367 ContentResolver res = getContext().getContentResolver(); 3368 res.notifyChange(Uri.parse("content://media/"), null); 3369 } 3370 } 3371 3372 /* 3373 * Rescan files for missing metadata and set their type accordingly. 3374 * There is code for detecting the removal of a nomedia file or renaming of 3375 * a directory from hidden to non-hidden in the MediaScanner and MtpDatabase, 3376 * both of which call here. 3377 */ 3378 private void processRemovedNoMediaPath(final String path) { 3379 final DatabaseHelper helper; 3380 if (path.startsWith(mExternalStoragePaths[0])) { 3381 helper = getDatabaseForUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI); 3382 } else { 3383 helper = getDatabaseForUri(MediaStore.Audio.Media.INTERNAL_CONTENT_URI); 3384 } 3385 SQLiteDatabase db = helper.getWritableDatabase(); 3386 new ScannerClient(getContext(), db, path); 3387 } 3388 3389 private static final class ScannerClient implements MediaScannerConnectionClient { 3390 String mPath = null; 3391 MediaScannerConnection mScannerConnection; 3392 SQLiteDatabase mDb; 3393 3394 public ScannerClient(Context context, SQLiteDatabase db, String path) { 3395 mDb = db; 3396 mPath = path; 3397 mScannerConnection = new MediaScannerConnection(context, this); 3398 mScannerConnection.connect(); 3399 } 3400 3401 @Override 3402 public void onMediaScannerConnected() { 3403 Cursor c = mDb.query("files", openFileColumns, "_data like ?", 3404 new String[] { mPath + "/%"}, null, null, null); 3405 while (c.moveToNext()) { 3406 String d = c.getString(0); 3407 File f = new File(d); 3408 if (f.isFile()) { 3409 mScannerConnection.scanFile(d, null); 3410 } 3411 } 3412 mScannerConnection.disconnect(); 3413 c.close(); 3414 } 3415 3416 @Override 3417 public void onScanCompleted(String path, Uri uri) { 3418 } 3419 } 3420 3421 @Override 3422 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 3423 throws OperationApplicationException { 3424 3425 // The operations array provides no overall information about the URI(s) being operated 3426 // on, so begin a transaction for ALL of the databases. 3427 DatabaseHelper ihelper = getDatabaseForUri(MediaStore.Audio.Media.INTERNAL_CONTENT_URI); 3428 DatabaseHelper ehelper = getDatabaseForUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI); 3429 SQLiteDatabase idb = ihelper.getWritableDatabase(); 3430 idb.beginTransaction(); 3431 SQLiteDatabase edb = null; 3432 if (ehelper != null) { 3433 edb = ehelper.getWritableDatabase(); 3434 edb.beginTransaction(); 3435 } 3436 try { 3437 ContentProviderResult[] result = super.applyBatch(operations); 3438 idb.setTransactionSuccessful(); 3439 if (edb != null) { 3440 edb.setTransactionSuccessful(); 3441 } 3442 // Rather than sending targeted change notifications for every Uri 3443 // affected by the batch operation, just invalidate the entire internal 3444 // and external name space. 3445 ContentResolver res = getContext().getContentResolver(); 3446 res.notifyChange(Uri.parse("content://media/"), null); 3447 return result; 3448 } finally { 3449 idb.endTransaction(); 3450 if (edb != null) { 3451 edb.endTransaction(); 3452 } 3453 } 3454 } 3455 3456 3457 private MediaThumbRequest requestMediaThumbnail(String path, Uri uri, int priority, long magic) { 3458 synchronized (mMediaThumbQueue) { 3459 MediaThumbRequest req = null; 3460 try { 3461 req = new MediaThumbRequest( 3462 getContext().getContentResolver(), path, uri, priority, magic); 3463 mMediaThumbQueue.add(req); 3464 // Trigger the handler. 3465 Message msg = mThumbHandler.obtainMessage(IMAGE_THUMB); 3466 msg.sendToTarget(); 3467 } catch (Throwable t) { 3468 Log.w(TAG, t); 3469 } 3470 return req; 3471 } 3472 } 3473 3474 private String generateFileName(boolean internal, String preferredExtension, String directoryName) 3475 { 3476 // create a random file 3477 String name = String.valueOf(System.currentTimeMillis()); 3478 3479 if (internal) { 3480 throw new UnsupportedOperationException("Writing to internal storage is not supported."); 3481// return Environment.getDataDirectory() 3482// + "/" + directoryName + "/" + name + preferredExtension; 3483 } else { 3484 return mExternalStoragePaths[0] + "/" + directoryName + "/" + name + preferredExtension; 3485 } 3486 } 3487 3488 private boolean ensureFileExists(String path) { 3489 File file = new File(path); 3490 if (file.exists()) { 3491 return true; 3492 } else { 3493 // we will not attempt to create the first directory in the path 3494 // (for example, do not create /sdcard if the SD card is not mounted) 3495 int secondSlash = path.indexOf('/', 1); 3496 if (secondSlash < 1) return false; 3497 String directoryPath = path.substring(0, secondSlash); 3498 File directory = new File(directoryPath); 3499 if (!directory.exists()) 3500 return false; 3501 file.getParentFile().mkdirs(); 3502 try { 3503 return file.createNewFile(); 3504 } catch(IOException ioe) { 3505 Log.e(TAG, "File creation failed", ioe); 3506 } 3507 return false; 3508 } 3509 } 3510 3511 private static final class GetTableAndWhereOutParameter { 3512 public String table; 3513 public String where; 3514 } 3515 3516 static final GetTableAndWhereOutParameter sGetTableAndWhereParam = 3517 new GetTableAndWhereOutParameter(); 3518 3519 private void getTableAndWhere(Uri uri, int match, String userWhere, 3520 GetTableAndWhereOutParameter out) { 3521 String where = null; 3522 switch (match) { 3523 case IMAGES_MEDIA: 3524 out.table = "files"; 3525 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_IMAGE; 3526 break; 3527 3528 case IMAGES_MEDIA_ID: 3529 out.table = "files"; 3530 where = "_id = " + uri.getPathSegments().get(3); 3531 break; 3532 3533 case IMAGES_THUMBNAILS_ID: 3534 where = "_id=" + uri.getPathSegments().get(3); 3535 case IMAGES_THUMBNAILS: 3536 out.table = "thumbnails"; 3537 break; 3538 3539 case AUDIO_MEDIA: 3540 out.table = "files"; 3541 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_AUDIO; 3542 break; 3543 3544 case AUDIO_MEDIA_ID: 3545 out.table = "files"; 3546 where = "_id=" + uri.getPathSegments().get(3); 3547 break; 3548 3549 case AUDIO_MEDIA_ID_GENRES: 3550 out.table = "audio_genres"; 3551 where = "audio_id=" + uri.getPathSegments().get(3); 3552 break; 3553 3554 case AUDIO_MEDIA_ID_GENRES_ID: 3555 out.table = "audio_genres"; 3556 where = "audio_id=" + uri.getPathSegments().get(3) + 3557 " AND genre_id=" + uri.getPathSegments().get(5); 3558 break; 3559 3560 case AUDIO_MEDIA_ID_PLAYLISTS: 3561 out.table = "audio_playlists"; 3562 where = "audio_id=" + uri.getPathSegments().get(3); 3563 break; 3564 3565 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 3566 out.table = "audio_playlists"; 3567 where = "audio_id=" + uri.getPathSegments().get(3) + 3568 " AND playlists_id=" + uri.getPathSegments().get(5); 3569 break; 3570 3571 case AUDIO_GENRES: 3572 out.table = "audio_genres"; 3573 break; 3574 3575 case AUDIO_GENRES_ID: 3576 out.table = "audio_genres"; 3577 where = "_id=" + uri.getPathSegments().get(3); 3578 break; 3579 3580 case AUDIO_GENRES_ID_MEMBERS: 3581 out.table = "audio_genres"; 3582 where = "genre_id=" + uri.getPathSegments().get(3); 3583 break; 3584 3585 case AUDIO_PLAYLISTS: 3586 out.table = "files"; 3587 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_PLAYLIST; 3588 break; 3589 3590 case AUDIO_PLAYLISTS_ID: 3591 out.table = "files"; 3592 where = "_id=" + uri.getPathSegments().get(3); 3593 break; 3594 3595 case AUDIO_PLAYLISTS_ID_MEMBERS: 3596 out.table = "audio_playlists_map"; 3597 where = "playlist_id=" + uri.getPathSegments().get(3); 3598 break; 3599 3600 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 3601 out.table = "audio_playlists_map"; 3602 where = "playlist_id=" + uri.getPathSegments().get(3) + 3603 " AND _id=" + uri.getPathSegments().get(5); 3604 break; 3605 3606 case AUDIO_ALBUMART_ID: 3607 out.table = "album_art"; 3608 where = "album_id=" + uri.getPathSegments().get(3); 3609 break; 3610 3611 case VIDEO_MEDIA: 3612 out.table = "files"; 3613 where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_VIDEO; 3614 break; 3615 3616 case VIDEO_MEDIA_ID: 3617 out.table = "files"; 3618 where = "_id=" + uri.getPathSegments().get(3); 3619 break; 3620 3621 case VIDEO_THUMBNAILS_ID: 3622 where = "_id=" + uri.getPathSegments().get(3); 3623 case VIDEO_THUMBNAILS: 3624 out.table = "videothumbnails"; 3625 break; 3626 3627 case FILES_ID: 3628 case MTP_OBJECTS_ID: 3629 where = "_id=" + uri.getPathSegments().get(2); 3630 case FILES: 3631 case MTP_OBJECTS: 3632 out.table = "files"; 3633 break; 3634 3635 default: 3636 throw new UnsupportedOperationException( 3637 "Unknown or unsupported URL: " + uri.toString()); 3638 } 3639 3640 // Add in the user requested WHERE clause, if needed 3641 if (!TextUtils.isEmpty(userWhere)) { 3642 if (!TextUtils.isEmpty(where)) { 3643 out.where = where + " AND (" + userWhere + ")"; 3644 } else { 3645 out.where = userWhere; 3646 } 3647 } else { 3648 out.where = where; 3649 } 3650 } 3651 3652 @Override 3653 public int delete(Uri uri, String userWhere, String[] whereArgs) { 3654 int count; 3655 int match = URI_MATCHER.match(uri); 3656 3657 // handle MEDIA_SCANNER before calling getDatabaseForUri() 3658 if (match == MEDIA_SCANNER) { 3659 if (mMediaScannerVolume == null) { 3660 return 0; 3661 } 3662 DatabaseHelper database = getDatabaseForUri( 3663 Uri.parse("content://media/" + mMediaScannerVolume + "/audio")); 3664 if (database == null) { 3665 Log.w(TAG, "no database for scanned volume " + mMediaScannerVolume); 3666 } else { 3667 database.mScanStopTime = SystemClock.currentTimeMicro(); 3668 } 3669 mMediaScannerVolume = null; 3670 return 1; 3671 } 3672 3673 if (match == VOLUMES_ID) { 3674 detachVolume(uri); 3675 count = 1; 3676 } else if (match == MTP_CONNECTED) { 3677 synchronized (mMtpServiceConnection) { 3678 if (mMtpService != null) { 3679 // MTP has disconnected, so release our connection to MtpService 3680 getContext().unbindService(mMtpServiceConnection); 3681 count = 1; 3682 // mMtpServiceConnection.onServiceDisconnected might not get called, 3683 // so set mMtpService = null here 3684 mMtpService = null; 3685 } else { 3686 count = 0; 3687 } 3688 } 3689 } else { 3690 DatabaseHelper database = getDatabaseForUri(uri); 3691 if (database == null) { 3692 throw new UnsupportedOperationException( 3693 "Unknown URI: " + uri + " match: " + match); 3694 } 3695 database.mNumDeletes++; 3696 SQLiteDatabase db = database.getWritableDatabase(); 3697 3698 synchronized (sGetTableAndWhereParam) { 3699 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 3700 3701 if (sGetTableAndWhereParam.table.equals("files")) { 3702 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA); 3703 if (deleteparam == null || ! deleteparam.equals("false")) { 3704 database.mNumQueries++; 3705 Cursor c = db.query(sGetTableAndWhereParam.table, 3706 sMediaTypeDataId, 3707 sGetTableAndWhereParam.where, whereArgs, null, null, null); 3708 String [] idvalue = new String[] { "" }; 3709 String [] playlistvalues = new String[] { "", "" }; 3710 while (c.moveToNext()) { 3711 int mediatype = c.getInt(0); 3712 if (mediatype == FileColumns.MEDIA_TYPE_IMAGE) { 3713 try { 3714 Libcore.os.remove(c.getString(1)); 3715 idvalue[0] = "" + c.getLong(2); 3716 database.mNumQueries++; 3717 Cursor cc = db.query("thumbnails", sDataOnlyColumn, 3718 "image_id=?", idvalue, null, null, null); 3719 while (cc.moveToNext()) { 3720 Libcore.os.remove(cc.getString(0)); 3721 } 3722 cc.close(); 3723 database.mNumDeletes++; 3724 db.delete("thumbnails", "image_id=?", idvalue); 3725 } catch (ErrnoException e) { 3726 } 3727 } else if (mediatype == FileColumns.MEDIA_TYPE_VIDEO) { 3728 try { 3729 Libcore.os.remove(c.getString(1)); 3730 } catch (ErrnoException e) { 3731 } 3732 } else if (mediatype == FileColumns.MEDIA_TYPE_AUDIO) { 3733 if (!database.mInternal) { 3734 idvalue[0] = "" + c.getLong(2); 3735 db.delete("audio_genres_map", "audio_id=?", idvalue); 3736 // for each playlist that the item appears in, move 3737 // all the items behind it forward by one 3738 Cursor cc = db.query("audio_playlists_map", 3739 sPlaylistIdPlayOrder, 3740 "audio_id=?", idvalue, null, null, null); 3741 while (cc.moveToNext()) { 3742 playlistvalues[0] = "" + cc.getLong(0); 3743 playlistvalues[1] = "" + cc.getInt(1); 3744 db.execSQL("UPDATE audio_playlists_map" + 3745 " SET play_order=play_order-1" + 3746 " WHERE playlist_id=? AND play_order>?", 3747 playlistvalues); 3748 } 3749 cc.close(); 3750 db.delete("audio_playlists_map", "audio_id=?", idvalue); 3751 } 3752 } else if (mediatype == FileColumns.MEDIA_TYPE_PLAYLIST) { 3753 // TODO, maybe: remove the audio_playlists_cleanup trigger and implement 3754 // it functionality here (clean up the playlist map) 3755 } 3756 } 3757 c.close(); 3758 } 3759 } 3760 3761 switch (match) { 3762 case MTP_OBJECTS: 3763 case MTP_OBJECTS_ID: 3764 try { 3765 // don't send objectRemoved event since this originated from MTP 3766 mDisableMtpObjectCallbacks = true; 3767 database.mNumDeletes++; 3768 count = db.delete("files", sGetTableAndWhereParam.where, whereArgs); 3769 } finally { 3770 mDisableMtpObjectCallbacks = false; 3771 } 3772 break; 3773 case AUDIO_GENRES_ID_MEMBERS: 3774 database.mNumDeletes++; 3775 count = db.delete("audio_genres_map", 3776 sGetTableAndWhereParam.where, whereArgs); 3777 break; 3778 3779 case IMAGES_THUMBNAILS_ID: 3780 case IMAGES_THUMBNAILS: 3781 case VIDEO_THUMBNAILS_ID: 3782 case VIDEO_THUMBNAILS: 3783 // Delete the referenced files first. 3784 Cursor c = db.query(sGetTableAndWhereParam.table, 3785 sDataOnlyColumn, 3786 sGetTableAndWhereParam.where, whereArgs, null, null, null); 3787 if (c != null) { 3788 while (c.moveToNext()) { 3789 try { 3790 Libcore.os.remove(c.getString(0)); 3791 } catch (ErrnoException e) { 3792 } 3793 } 3794 c.close(); 3795 } 3796 database.mNumDeletes++; 3797 count = db.delete(sGetTableAndWhereParam.table, 3798 sGetTableAndWhereParam.where, whereArgs); 3799 break; 3800 3801 default: 3802 database.mNumDeletes++; 3803 count = db.delete(sGetTableAndWhereParam.table, 3804 sGetTableAndWhereParam.where, whereArgs); 3805 break; 3806 } 3807 // Since there are multiple Uris that can refer to the same files 3808 // and deletes can affect other objects in storage (like subdirectories 3809 // or playlists) we will notify a change on the entire volume to make 3810 // sure no listeners miss the notification. 3811 String volume = uri.getPathSegments().get(0); 3812 Uri notifyUri = Uri.parse("content://" + MediaStore.AUTHORITY + "/" + volume); 3813 getContext().getContentResolver().notifyChange(notifyUri, null); 3814 } 3815 } 3816 3817 return count; 3818 } 3819 3820 @Override 3821 public Bundle call(String method, String arg, Bundle extras) { 3822 if (MediaStore.UNHIDE_CALL.equals(method)) { 3823 processRemovedNoMediaPath(arg); 3824 return null; 3825 } 3826 throw new UnsupportedOperationException("Unsupported call: " + method); 3827 } 3828 3829 @Override 3830 public int update(Uri uri, ContentValues initialValues, String userWhere, 3831 String[] whereArgs) { 3832 int count; 3833 // Log.v(TAG, "update for uri="+uri+", initValues="+initialValues); 3834 int match = URI_MATCHER.match(uri); 3835 DatabaseHelper helper = getDatabaseForUri(uri); 3836 if (helper == null) { 3837 throw new UnsupportedOperationException( 3838 "Unknown URI: " + uri); 3839 } 3840 helper.mNumUpdates++; 3841 3842 SQLiteDatabase db = helper.getWritableDatabase(); 3843 3844 String genre = null; 3845 if (initialValues != null) { 3846 genre = initialValues.getAsString(Audio.AudioColumns.GENRE); 3847 initialValues.remove(Audio.AudioColumns.GENRE); 3848 } 3849 3850 synchronized (sGetTableAndWhereParam) { 3851 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 3852 3853 // special case renaming directories via MTP. 3854 // in this case we must update all paths in the database with 3855 // the directory name as a prefix 3856 if ((match == MTP_OBJECTS || match == MTP_OBJECTS_ID) 3857 && initialValues != null && initialValues.size() == 1) { 3858 String oldPath = null; 3859 String newPath = initialValues.getAsString(MediaStore.MediaColumns.DATA); 3860 mDirectoryCache.remove(newPath); 3861 // MtpDatabase will rename the directory first, so we test the new file name 3862 File f = new File(newPath); 3863 if (newPath != null && f.isDirectory()) { 3864 helper.mNumQueries++; 3865 Cursor cursor = db.query(sGetTableAndWhereParam.table, PATH_PROJECTION, 3866 userWhere, whereArgs, null, null, null); 3867 try { 3868 if (cursor != null && cursor.moveToNext()) { 3869 oldPath = cursor.getString(1); 3870 } 3871 } finally { 3872 if (cursor != null) cursor.close(); 3873 } 3874 if (oldPath != null) { 3875 mDirectoryCache.remove(oldPath); 3876 // first rename the row for the directory 3877 helper.mNumUpdates++; 3878 count = db.update(sGetTableAndWhereParam.table, initialValues, 3879 sGetTableAndWhereParam.where, whereArgs); 3880 if (count > 0) { 3881 // then update the paths of any files and folders contained in the directory. 3882 Object[] bindArgs = new Object[] {oldPath + "/%", oldPath.length() + 1, newPath}; 3883 helper.mNumUpdates++; 3884 db.execSQL("UPDATE files SET _data=?3||SUBSTR(_data, ?2) WHERE _data LIKE ?1;", bindArgs); 3885 } 3886 3887 if (count > 0 && !db.inTransaction()) { 3888 getContext().getContentResolver().notifyChange(uri, null); 3889 } 3890 if (f.getName().startsWith(".")) { 3891 // the new directory name is hidden 3892 processNewNoMediaPath(helper, db, newPath); 3893 } 3894 return count; 3895 } 3896 } else if (newPath.toLowerCase(Locale.US).endsWith("/.nomedia")) { 3897 processNewNoMediaPath(helper, db, newPath); 3898 } 3899 } 3900 3901 switch (match) { 3902 case AUDIO_MEDIA: 3903 case AUDIO_MEDIA_ID: 3904 { 3905 ContentValues values = new ContentValues(initialValues); 3906 String albumartist = values.getAsString(MediaStore.Audio.Media.ALBUM_ARTIST); 3907 String compilation = values.getAsString(MediaStore.Audio.Media.COMPILATION); 3908 values.remove(MediaStore.Audio.Media.COMPILATION); 3909 3910 // Insert the artist into the artist table and remove it from 3911 // the input values 3912 String artist = values.getAsString("artist"); 3913 values.remove("artist"); 3914 if (artist != null) { 3915 long artistRowId; 3916 HashMap<String, Long> artistCache = helper.mArtistCache; 3917 synchronized(artistCache) { 3918 Long temp = artistCache.get(artist); 3919 if (temp == null) { 3920 artistRowId = getKeyIdForName(helper, db, 3921 "artists", "artist_key", "artist", 3922 artist, artist, null, 0, null, artistCache, uri); 3923 } else { 3924 artistRowId = temp.longValue(); 3925 } 3926 } 3927 values.put("artist_id", Integer.toString((int)artistRowId)); 3928 } 3929 3930 // Do the same for the album field. 3931 String so = values.getAsString("album"); 3932 values.remove("album"); 3933 if (so != null) { 3934 String path = values.getAsString(MediaStore.MediaColumns.DATA); 3935 int albumHash = 0; 3936 if (albumartist != null) { 3937 albumHash = albumartist.hashCode(); 3938 } else if (compilation != null && compilation.equals("1")) { 3939 // nothing to do, hash already set 3940 } else { 3941 if (path == null) { 3942 if (match == AUDIO_MEDIA) { 3943 Log.w(TAG, "Possible multi row album name update without" 3944 + " path could give wrong album key"); 3945 } else { 3946 //Log.w(TAG, "Specify path to avoid extra query"); 3947 Cursor c = query(uri, 3948 new String[] { MediaStore.Audio.Media.DATA}, 3949 null, null, null); 3950 if (c != null) { 3951 try { 3952 int numrows = c.getCount(); 3953 if (numrows == 1) { 3954 c.moveToFirst(); 3955 path = c.getString(0); 3956 } else { 3957 Log.e(TAG, "" + numrows + " rows for " + uri); 3958 } 3959 } finally { 3960 c.close(); 3961 } 3962 } 3963 } 3964 } 3965 if (path != null) { 3966 albumHash = path.substring(0, path.lastIndexOf('/')).hashCode(); 3967 } 3968 } 3969 3970 String s = so.toString(); 3971 long albumRowId; 3972 HashMap<String, Long> albumCache = helper.mAlbumCache; 3973 synchronized(albumCache) { 3974 String cacheName = s + albumHash; 3975 Long temp = albumCache.get(cacheName); 3976 if (temp == null) { 3977 albumRowId = getKeyIdForName(helper, db, 3978 "albums", "album_key", "album", 3979 s, cacheName, path, albumHash, artist, albumCache, uri); 3980 } else { 3981 albumRowId = temp.longValue(); 3982 } 3983 } 3984 values.put("album_id", Integer.toString((int)albumRowId)); 3985 } 3986 3987 // don't allow the title_key field to be updated directly 3988 values.remove("title_key"); 3989 // If the title field is modified, update the title_key 3990 so = values.getAsString("title"); 3991 if (so != null) { 3992 String s = so.toString(); 3993 values.put("title_key", MediaStore.Audio.keyFor(s)); 3994 // do a final trim of the title, in case it started with the special 3995 // "sort first" character (ascii \001) 3996 values.remove("title"); 3997 values.put("title", s.trim()); 3998 } 3999 4000 helper.mNumUpdates++; 4001 count = db.update(sGetTableAndWhereParam.table, values, 4002 sGetTableAndWhereParam.where, whereArgs); 4003 if (genre != null) { 4004 if (count == 1 && match == AUDIO_MEDIA_ID) { 4005 long rowId = Long.parseLong(uri.getPathSegments().get(3)); 4006 updateGenre(rowId, genre); 4007 } else { 4008 // can't handle genres for bulk update or for non-audio files 4009 Log.w(TAG, "ignoring genre in update: count = " 4010 + count + " match = " + match); 4011 } 4012 } 4013 } 4014 break; 4015 case IMAGES_MEDIA: 4016 case IMAGES_MEDIA_ID: 4017 case VIDEO_MEDIA: 4018 case VIDEO_MEDIA_ID: 4019 { 4020 ContentValues values = new ContentValues(initialValues); 4021 // Don't allow bucket id or display name to be updated directly. 4022 // The same names are used for both images and table columns, so 4023 // we use the ImageColumns constants here. 4024 values.remove(ImageColumns.BUCKET_ID); 4025 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 4026 // If the data is being modified update the bucket values 4027 String data = values.getAsString(MediaColumns.DATA); 4028 if (data != null) { 4029 computeBucketValues(data, values); 4030 } 4031 computeTakenTime(values); 4032 helper.mNumUpdates++; 4033 count = db.update(sGetTableAndWhereParam.table, values, 4034 sGetTableAndWhereParam.where, whereArgs); 4035 // if this is a request from MediaScanner, DATA should contains file path 4036 // we only process update request from media scanner, otherwise the requests 4037 // could be duplicate. 4038 if (count > 0 && values.getAsString(MediaStore.MediaColumns.DATA) != null) { 4039 helper.mNumQueries++; 4040 Cursor c = db.query(sGetTableAndWhereParam.table, 4041 READY_FLAG_PROJECTION, sGetTableAndWhereParam.where, 4042 whereArgs, null, null, null); 4043 if (c != null) { 4044 try { 4045 while (c.moveToNext()) { 4046 long magic = c.getLong(2); 4047 if (magic == 0) { 4048 requestMediaThumbnail(c.getString(1), uri, 4049 MediaThumbRequest.PRIORITY_NORMAL, 0); 4050 } 4051 } 4052 } finally { 4053 c.close(); 4054 } 4055 } 4056 } 4057 } 4058 break; 4059 4060 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 4061 String moveit = uri.getQueryParameter("move"); 4062 if (moveit != null) { 4063 String key = MediaStore.Audio.Playlists.Members.PLAY_ORDER; 4064 if (initialValues.containsKey(key)) { 4065 int newpos = initialValues.getAsInteger(key); 4066 List <String> segments = uri.getPathSegments(); 4067 long playlist = Long.valueOf(segments.get(3)); 4068 int oldpos = Integer.valueOf(segments.get(5)); 4069 return movePlaylistEntry(helper, db, playlist, oldpos, newpos); 4070 } 4071 throw new IllegalArgumentException("Need to specify " + key + 4072 " when using 'move' parameter"); 4073 } 4074 // fall through 4075 default: 4076 helper.mNumUpdates++; 4077 count = db.update(sGetTableAndWhereParam.table, initialValues, 4078 sGetTableAndWhereParam.where, whereArgs); 4079 break; 4080 } 4081 } 4082 // in a transaction, the code that began the transaction should be taking 4083 // care of notifications once it ends the transaction successfully 4084 if (count > 0 && !db.inTransaction()) { 4085 getContext().getContentResolver().notifyChange(uri, null); 4086 } 4087 return count; 4088 } 4089 4090 private int movePlaylistEntry(DatabaseHelper helper, SQLiteDatabase db, 4091 long playlist, int from, int to) { 4092 if (from == to) { 4093 return 0; 4094 } 4095 db.beginTransaction(); 4096 try { 4097 int numlines = 0; 4098 helper.mNumUpdates += 3; 4099 Cursor c = db.query("audio_playlists_map", 4100 new String [] {"play_order" }, 4101 "playlist_id=?", new String[] {"" + playlist}, null, null, "play_order", 4102 from + ",1"); 4103 c.moveToFirst(); 4104 int from_play_order = c.getInt(0); 4105 c.close(); 4106 c = db.query("audio_playlists_map", 4107 new String [] {"play_order" }, 4108 "playlist_id=?", new String[] {"" + playlist}, null, null, "play_order", 4109 to + ",1"); 4110 c.moveToFirst(); 4111 int to_play_order = c.getInt(0); 4112 c.close(); 4113 db.execSQL("UPDATE audio_playlists_map SET play_order=-1" + 4114 " WHERE play_order=" + from_play_order + 4115 " AND playlist_id=" + playlist); 4116 // We could just run both of the next two statements, but only one of 4117 // of them will actually do anything, so might as well skip the compile 4118 // and execute steps. 4119 if (from < to) { 4120 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order-1" + 4121 " WHERE play_order<=" + to_play_order + 4122 " AND play_order>" + from_play_order + 4123 " AND playlist_id=" + playlist); 4124 numlines = to - from + 1; 4125 } else { 4126 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order+1" + 4127 " WHERE play_order>=" + to_play_order + 4128 " AND play_order<" + from_play_order + 4129 " AND playlist_id=" + playlist); 4130 numlines = from - to + 1; 4131 } 4132 db.execSQL("UPDATE audio_playlists_map SET play_order=" + to_play_order + 4133 " WHERE play_order=-1 AND playlist_id=" + playlist); 4134 db.setTransactionSuccessful(); 4135 Uri uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI 4136 .buildUpon().appendEncodedPath(String.valueOf(playlist)).build(); 4137 getContext().getContentResolver().notifyChange(uri, null); 4138 return numlines; 4139 } finally { 4140 db.endTransaction(); 4141 } 4142 } 4143 4144 private static final String[] openFileColumns = new String[] { 4145 MediaStore.MediaColumns.DATA, 4146 }; 4147 4148 @Override 4149 public ParcelFileDescriptor openFile(Uri uri, String mode) 4150 throws FileNotFoundException { 4151 4152 ParcelFileDescriptor pfd = null; 4153 4154 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_FILE_ID) { 4155 // get album art for the specified media file 4156 DatabaseHelper database = getDatabaseForUri(uri); 4157 if (database == null) { 4158 throw new IllegalStateException("Couldn't open database for " + uri); 4159 } 4160 SQLiteDatabase db = database.getReadableDatabase(); 4161 if (db == null) { 4162 throw new IllegalStateException("Couldn't open database for " + uri); 4163 } 4164 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4165 int songid = Integer.parseInt(uri.getPathSegments().get(3)); 4166 qb.setTables("audio_meta"); 4167 qb.appendWhere("_id=" + songid); 4168 Cursor c = qb.query(db, 4169 new String [] { 4170 MediaStore.Audio.Media.DATA, 4171 MediaStore.Audio.Media.ALBUM_ID }, 4172 null, null, null, null, null); 4173 if (c.moveToFirst()) { 4174 String audiopath = c.getString(0); 4175 int albumid = c.getInt(1); 4176 // Try to get existing album art for this album first, which 4177 // could possibly have been obtained from a different file. 4178 // If that fails, try to get it from this specific file. 4179 Uri newUri = ContentUris.withAppendedId(ALBUMART_URI, albumid); 4180 try { 4181 pfd = openFileAndEnforcePathPermissionsHelper(newUri, mode); 4182 } catch (FileNotFoundException ex) { 4183 // That didn't work, now try to get it from the specific file 4184 pfd = getThumb(database, db, audiopath, albumid, null); 4185 } 4186 } 4187 c.close(); 4188 return pfd; 4189 } 4190 4191 try { 4192 pfd = openFileAndEnforcePathPermissionsHelper(uri, mode); 4193 } catch (FileNotFoundException ex) { 4194 if (mode.contains("w")) { 4195 // if the file couldn't be created, we shouldn't extract album art 4196 throw ex; 4197 } 4198 4199 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_ID) { 4200 // Tried to open an album art file which does not exist. Regenerate. 4201 DatabaseHelper database = getDatabaseForUri(uri); 4202 if (database == null) { 4203 throw ex; 4204 } 4205 SQLiteDatabase db = database.getReadableDatabase(); 4206 if (db == null) { 4207 throw new IllegalStateException("Couldn't open database for " + uri); 4208 } 4209 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4210 int albumid = Integer.parseInt(uri.getPathSegments().get(3)); 4211 qb.setTables("audio_meta"); 4212 qb.appendWhere("album_id=" + albumid); 4213 Cursor c = qb.query(db, 4214 new String [] { 4215 MediaStore.Audio.Media.DATA }, 4216 null, null, null, null, MediaStore.Audio.Media.TRACK); 4217 if (c.moveToFirst()) { 4218 String audiopath = c.getString(0); 4219 pfd = getThumb(database, db, audiopath, albumid, uri); 4220 } 4221 c.close(); 4222 } 4223 if (pfd == null) { 4224 throw ex; 4225 } 4226 } 4227 return pfd; 4228 } 4229 4230 /** 4231 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 4232 */ 4233 private File queryForDataFile(Uri uri) throws FileNotFoundException { 4234 final Cursor cursor = query( 4235 uri, new String[] { MediaColumns.DATA }, null, null, null); 4236 try { 4237 switch (cursor.getCount()) { 4238 case 0: 4239 throw new FileNotFoundException("No entry for " + uri); 4240 case 1: 4241 if (cursor.moveToFirst()) { 4242 return new File(cursor.getString(0)); 4243 } else { 4244 throw new FileNotFoundException("Unable to read entry for " + uri); 4245 } 4246 default: 4247 throw new FileNotFoundException("Multiple items at " + uri); 4248 } 4249 } finally { 4250 cursor.close(); 4251 } 4252 } 4253 4254 /** 4255 * Replacement for {@link #openFileHelper(Uri, String)} which enforces any 4256 * permissions applicable to the path before returning. 4257 */ 4258 private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, String mode) 4259 throws FileNotFoundException { 4260 final int modeBits = ContentResolver.modeToMode(uri, mode); 4261 final boolean isWrite = (modeBits & MODE_WRITE_ONLY) != 0; 4262 4263 final File file = queryForDataFile(uri); 4264 final String path; 4265 try { 4266 path = file.getCanonicalPath(); 4267 } catch (IOException e) { 4268 throw new IllegalArgumentException("Unable to resolve canonical path for " + file, e); 4269 } 4270 4271 if (path.startsWith(sExternalPath)) { 4272 getContext().enforceCallingOrSelfPermission( 4273 READ_EXTERNAL_STORAGE, "External path: " + path); 4274 4275 if (isWrite) { 4276 getContext().enforceCallingOrSelfPermission( 4277 WRITE_EXTERNAL_STORAGE, "External path: " + path); 4278 } 4279 } else if (path.startsWith(sCachePath)) { 4280 getContext().enforceCallingOrSelfPermission( 4281 ACCESS_CACHE_FILESYSTEM, "Cache path: " + path); 4282 } 4283 4284 return ParcelFileDescriptor.open(file, modeBits); 4285 } 4286 4287 private class ThumbData { 4288 DatabaseHelper helper; 4289 SQLiteDatabase db; 4290 String path; 4291 long album_id; 4292 Uri albumart_uri; 4293 } 4294 4295 private void makeThumbAsync(DatabaseHelper helper, SQLiteDatabase db, 4296 String path, long album_id) { 4297 synchronized (mPendingThumbs) { 4298 if (mPendingThumbs.contains(path)) { 4299 // There's already a request to make an album art thumbnail 4300 // for this audio file in the queue. 4301 return; 4302 } 4303 4304 mPendingThumbs.add(path); 4305 } 4306 4307 ThumbData d = new ThumbData(); 4308 d.helper = helper; 4309 d.db = db; 4310 d.path = path; 4311 d.album_id = album_id; 4312 d.albumart_uri = ContentUris.withAppendedId(mAlbumArtBaseUri, album_id); 4313 4314 // Instead of processing thumbnail requests in the order they were 4315 // received we instead process them stack-based, i.e. LIFO. 4316 // The idea behind this is that the most recently requested thumbnails 4317 // are most likely the ones still in the user's view, whereas those 4318 // requested earlier may have already scrolled off. 4319 synchronized (mThumbRequestStack) { 4320 mThumbRequestStack.push(d); 4321 } 4322 4323 // Trigger the handler. 4324 Message msg = mThumbHandler.obtainMessage(ALBUM_THUMB); 4325 msg.sendToTarget(); 4326 } 4327 4328 // Extract compressed image data from the audio file itself or, if that fails, 4329 // look for a file "AlbumArt.jpg" in the containing directory. 4330 private static byte[] getCompressedAlbumArt(Context context, String path) { 4331 byte[] compressed = null; 4332 4333 try { 4334 File f = new File(path); 4335 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, 4336 ParcelFileDescriptor.MODE_READ_ONLY); 4337 4338 MediaScanner scanner = new MediaScanner(context); 4339 compressed = scanner.extractAlbumArt(pfd.getFileDescriptor()); 4340 pfd.close(); 4341 4342 // If no embedded art exists, look for a suitable image file in the 4343 // same directory as the media file, except if that directory is 4344 // is the root directory of the sd card or the download directory. 4345 // We look for, in order of preference: 4346 // 0 AlbumArt.jpg 4347 // 1 AlbumArt*Large.jpg 4348 // 2 Any other jpg image with 'albumart' anywhere in the name 4349 // 3 Any other jpg image 4350 // 4 any other png image 4351 if (compressed == null && path != null) { 4352 int lastSlash = path.lastIndexOf('/'); 4353 if (lastSlash > 0) { 4354 4355 String artPath = path.substring(0, lastSlash); 4356 String sdroot = mExternalStoragePaths[0]; 4357 String dwndir = Environment.getExternalStoragePublicDirectory( 4358 Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); 4359 4360 String bestmatch = null; 4361 synchronized (sFolderArtMap) { 4362 if (sFolderArtMap.containsKey(artPath)) { 4363 bestmatch = sFolderArtMap.get(artPath); 4364 } else if (!artPath.equalsIgnoreCase(sdroot) && 4365 !artPath.equalsIgnoreCase(dwndir)) { 4366 File dir = new File(artPath); 4367 String [] entrynames = dir.list(); 4368 if (entrynames == null) { 4369 return null; 4370 } 4371 bestmatch = null; 4372 int matchlevel = 1000; 4373 for (int i = entrynames.length - 1; i >=0; i--) { 4374 String entry = entrynames[i].toLowerCase(); 4375 if (entry.equals("albumart.jpg")) { 4376 bestmatch = entrynames[i]; 4377 break; 4378 } else if (entry.startsWith("albumart") 4379 && entry.endsWith("large.jpg") 4380 && matchlevel > 1) { 4381 bestmatch = entrynames[i]; 4382 matchlevel = 1; 4383 } else if (entry.contains("albumart") 4384 && entry.endsWith(".jpg") 4385 && matchlevel > 2) { 4386 bestmatch = entrynames[i]; 4387 matchlevel = 2; 4388 } else if (entry.endsWith(".jpg") && matchlevel > 3) { 4389 bestmatch = entrynames[i]; 4390 matchlevel = 3; 4391 } else if (entry.endsWith(".png") && matchlevel > 4) { 4392 bestmatch = entrynames[i]; 4393 matchlevel = 4; 4394 } 4395 } 4396 // note that this may insert null if no album art was found 4397 sFolderArtMap.put(artPath, bestmatch); 4398 } 4399 } 4400 4401 if (bestmatch != null) { 4402 File file = new File(artPath, bestmatch); 4403 if (file.exists()) { 4404 compressed = new byte[(int)file.length()]; 4405 FileInputStream stream = null; 4406 try { 4407 stream = new FileInputStream(file); 4408 stream.read(compressed); 4409 } catch (IOException ex) { 4410 compressed = null; 4411 } finally { 4412 if (stream != null) { 4413 stream.close(); 4414 } 4415 } 4416 } 4417 } 4418 } 4419 } 4420 } catch (IOException e) { 4421 } 4422 4423 return compressed; 4424 } 4425 4426 // Return a URI to write the album art to and update the database as necessary. 4427 Uri getAlbumArtOutputUri(DatabaseHelper helper, SQLiteDatabase db, long album_id, Uri albumart_uri) { 4428 Uri out = null; 4429 // TODO: this could be done more efficiently with a call to db.replace(), which 4430 // replaces or inserts as needed, making it unnecessary to query() first. 4431 if (albumart_uri != null) { 4432 Cursor c = query(albumart_uri, new String [] { MediaStore.MediaColumns.DATA }, 4433 null, null, null); 4434 try { 4435 if (c != null && c.moveToFirst()) { 4436 String albumart_path = c.getString(0); 4437 if (ensureFileExists(albumart_path)) { 4438 out = albumart_uri; 4439 } 4440 } else { 4441 albumart_uri = null; 4442 } 4443 } finally { 4444 if (c != null) { 4445 c.close(); 4446 } 4447 } 4448 } 4449 if (albumart_uri == null){ 4450 ContentValues initialValues = new ContentValues(); 4451 initialValues.put("album_id", album_id); 4452 try { 4453 ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 4454 helper.mNumInserts++; 4455 long rowId = db.insert("album_art", MediaStore.MediaColumns.DATA, values); 4456 if (rowId > 0) { 4457 out = ContentUris.withAppendedId(ALBUMART_URI, rowId); 4458 } 4459 } catch (IllegalStateException ex) { 4460 Log.e(TAG, "error creating album thumb file"); 4461 } 4462 } 4463 return out; 4464 } 4465 4466 // Write out the album art to the output URI, recompresses the given Bitmap 4467 // if necessary, otherwise writes the compressed data. 4468 private void writeAlbumArt( 4469 boolean need_to_recompress, Uri out, byte[] compressed, Bitmap bm) { 4470 boolean success = false; 4471 try { 4472 OutputStream outstream = getContext().getContentResolver().openOutputStream(out); 4473 4474 if (!need_to_recompress) { 4475 // No need to recompress here, just write out the original 4476 // compressed data here. 4477 outstream.write(compressed); 4478 success = true; 4479 } else { 4480 success = bm.compress(Bitmap.CompressFormat.JPEG, 85, outstream); 4481 } 4482 4483 outstream.close(); 4484 } catch (FileNotFoundException ex) { 4485 Log.e(TAG, "error creating file", ex); 4486 } catch (IOException ex) { 4487 Log.e(TAG, "error creating file", ex); 4488 } 4489 if (!success) { 4490 // the thumbnail was not written successfully, delete the entry that refers to it 4491 getContext().getContentResolver().delete(out, null, null); 4492 } 4493 } 4494 4495 private ParcelFileDescriptor getThumb(DatabaseHelper helper, SQLiteDatabase db, String path, 4496 long album_id, Uri albumart_uri) { 4497 ThumbData d = new ThumbData(); 4498 d.helper = helper; 4499 d.db = db; 4500 d.path = path; 4501 d.album_id = album_id; 4502 d.albumart_uri = albumart_uri; 4503 return makeThumbInternal(d); 4504 } 4505 4506 private ParcelFileDescriptor makeThumbInternal(ThumbData d) { 4507 byte[] compressed = getCompressedAlbumArt(getContext(), d.path); 4508 4509 if (compressed == null) { 4510 return null; 4511 } 4512 4513 Bitmap bm = null; 4514 boolean need_to_recompress = true; 4515 4516 try { 4517 // get the size of the bitmap 4518 BitmapFactory.Options opts = new BitmapFactory.Options(); 4519 opts.inJustDecodeBounds = true; 4520 opts.inSampleSize = 1; 4521 BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts); 4522 4523 // request a reasonably sized output image 4524 final Resources r = getContext().getResources(); 4525 final int maximumThumbSize = r.getDimensionPixelSize(R.dimen.maximum_thumb_size); 4526 while (opts.outHeight > maximumThumbSize || opts.outWidth > maximumThumbSize) { 4527 opts.outHeight /= 2; 4528 opts.outWidth /= 2; 4529 opts.inSampleSize *= 2; 4530 } 4531 4532 if (opts.inSampleSize == 1) { 4533 // The original album art was of proper size, we won't have to 4534 // recompress the bitmap later. 4535 need_to_recompress = false; 4536 } else { 4537 // get the image for real now 4538 opts.inJustDecodeBounds = false; 4539 opts.inPreferredConfig = Bitmap.Config.RGB_565; 4540 bm = BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts); 4541 4542 if (bm != null && bm.getConfig() == null) { 4543 Bitmap nbm = bm.copy(Bitmap.Config.RGB_565, false); 4544 if (nbm != null && nbm != bm) { 4545 bm.recycle(); 4546 bm = nbm; 4547 } 4548 } 4549 } 4550 } catch (Exception e) { 4551 } 4552 4553 if (need_to_recompress && bm == null) { 4554 return null; 4555 } 4556 4557 if (d.albumart_uri == null) { 4558 // this one doesn't need to be saved (probably a song with an unknown album), 4559 // so stick it in a memory file and return that 4560 try { 4561 return ParcelFileDescriptor.fromData(compressed, "albumthumb"); 4562 } catch (IOException e) { 4563 } 4564 } else { 4565 // This one needs to actually be saved on the sd card. 4566 // This is wrapped in a transaction because there are various things 4567 // that could go wrong while generating the thumbnail, and we only want 4568 // to update the database when all steps succeeded. 4569 d.db.beginTransaction(); 4570 try { 4571 Uri out = getAlbumArtOutputUri(d.helper, d.db, d.album_id, d.albumart_uri); 4572 4573 if (out != null) { 4574 writeAlbumArt(need_to_recompress, out, compressed, bm); 4575 getContext().getContentResolver().notifyChange(MEDIA_URI, null); 4576 ParcelFileDescriptor pfd = openFileHelper(out, "r"); 4577 d.db.setTransactionSuccessful(); 4578 return pfd; 4579 } 4580 } catch (FileNotFoundException ex) { 4581 // do nothing, just return null below 4582 } catch (UnsupportedOperationException ex) { 4583 // do nothing, just return null below 4584 } finally { 4585 d.db.endTransaction(); 4586 if (bm != null) { 4587 bm.recycle(); 4588 } 4589 } 4590 } 4591 return null; 4592 } 4593 4594 /** 4595 * Look up the artist or album entry for the given name, creating that entry 4596 * if it does not already exists. 4597 * @param db The database 4598 * @param table The table to store the key/name pair in. 4599 * @param keyField The name of the key-column 4600 * @param nameField The name of the name-column 4601 * @param rawName The name that the calling app was trying to insert into the database 4602 * @param cacheName The string that will be inserted in to the cache 4603 * @param path The full path to the file being inserted in to the audio table 4604 * @param albumHash A hash to distinguish between different albums of the same name 4605 * @param artist The name of the artist, if known 4606 * @param cache The cache to add this entry to 4607 * @param srcuri The Uri that prompted the call to this method, used for determining whether this is 4608 * the internal or external database 4609 * @return The row ID for this artist/album, or -1 if the provided name was invalid 4610 */ 4611 private long getKeyIdForName(DatabaseHelper helper, SQLiteDatabase db, 4612 String table, String keyField, String nameField, 4613 String rawName, String cacheName, String path, int albumHash, 4614 String artist, HashMap<String, Long> cache, Uri srcuri) { 4615 long rowId; 4616 4617 if (rawName == null || rawName.length() == 0) { 4618 rawName = MediaStore.UNKNOWN_STRING; 4619 } 4620 String k = MediaStore.Audio.keyFor(rawName); 4621 4622 if (k == null) { 4623 // shouldn't happen, since we only get null keys for null inputs 4624 Log.e(TAG, "null key", new Exception()); 4625 return -1; 4626 } 4627 4628 boolean isAlbum = table.equals("albums"); 4629 boolean isUnknown = MediaStore.UNKNOWN_STRING.equals(rawName); 4630 4631 // To distinguish same-named albums, we append a hash. The hash is based 4632 // on the "album artist" tag if present, otherwise on the "compilation" tag 4633 // if present, otherwise on the path. 4634 // Ideally we would also take things like CDDB ID in to account, so 4635 // we can group files from the same album that aren't in the same 4636 // folder, but this is a quick and easy start that works immediately 4637 // without requiring support from the mp3, mp4 and Ogg meta data 4638 // readers, as long as the albums are in different folders. 4639 if (isAlbum) { 4640 k = k + albumHash; 4641 if (isUnknown) { 4642 k = k + artist; 4643 } 4644 } 4645 4646 String [] selargs = { k }; 4647 helper.mNumQueries++; 4648 Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null); 4649 4650 try { 4651 switch (c.getCount()) { 4652 case 0: { 4653 // insert new entry into table 4654 ContentValues otherValues = new ContentValues(); 4655 otherValues.put(keyField, k); 4656 otherValues.put(nameField, rawName); 4657 helper.mNumInserts++; 4658 rowId = db.insert(table, "duration", otherValues); 4659 if (path != null && isAlbum && ! isUnknown) { 4660 // We just inserted a new album. Now create an album art thumbnail for it. 4661 makeThumbAsync(helper, db, path, rowId); 4662 } 4663 if (rowId > 0) { 4664 String volume = srcuri.toString().substring(16, 24); // extract internal/external 4665 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 4666 getContext().getContentResolver().notifyChange(uri, null); 4667 } 4668 } 4669 break; 4670 case 1: { 4671 // Use the existing entry 4672 c.moveToFirst(); 4673 rowId = c.getLong(0); 4674 4675 // Determine whether the current rawName is better than what's 4676 // currently stored in the table, and update the table if it is. 4677 String currentFancyName = c.getString(2); 4678 String bestName = makeBestName(rawName, currentFancyName); 4679 if (!bestName.equals(currentFancyName)) { 4680 // update the table with the new name 4681 ContentValues newValues = new ContentValues(); 4682 newValues.put(nameField, bestName); 4683 helper.mNumUpdates++; 4684 db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null); 4685 String volume = srcuri.toString().substring(16, 24); // extract internal/external 4686 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 4687 getContext().getContentResolver().notifyChange(uri, null); 4688 } 4689 } 4690 break; 4691 default: 4692 // corrupt database 4693 Log.e(TAG, "Multiple entries in table " + table + " for key " + k); 4694 rowId = -1; 4695 break; 4696 } 4697 } finally { 4698 if (c != null) c.close(); 4699 } 4700 4701 if (cache != null && ! isUnknown) { 4702 cache.put(cacheName, rowId); 4703 } 4704 return rowId; 4705 } 4706 4707 /** 4708 * Returns the best string to use for display, given two names. 4709 * Note that this function does not necessarily return either one 4710 * of the provided names; it may decide to return a better alternative 4711 * (for example, specifying the inputs "Police" and "Police, The" will 4712 * return "The Police") 4713 * 4714 * The basic assumptions are: 4715 * - longer is better ("The police" is better than "Police") 4716 * - prefix is better ("The Police" is better than "Police, The") 4717 * - accents are better ("Motörhead" is better than "Motorhead") 4718 * 4719 * @param one The first of the two names to consider 4720 * @param two The last of the two names to consider 4721 * @return The actual name to use 4722 */ 4723 String makeBestName(String one, String two) { 4724 String name; 4725 4726 // Longer names are usually better. 4727 if (one.length() > two.length()) { 4728 name = one; 4729 } else { 4730 // Names with accents are usually better, and conveniently sort later 4731 if (one.toLowerCase().compareTo(two.toLowerCase()) > 0) { 4732 name = one; 4733 } else { 4734 name = two; 4735 } 4736 } 4737 4738 // Prefixes are better than postfixes. 4739 if (name.endsWith(", the") || name.endsWith(",the") || 4740 name.endsWith(", an") || name.endsWith(",an") || 4741 name.endsWith(", a") || name.endsWith(",a")) { 4742 String fix = name.substring(1 + name.lastIndexOf(',')); 4743 name = fix.trim() + " " + name.substring(0, name.lastIndexOf(',')); 4744 } 4745 4746 // TODO: word-capitalize the resulting name 4747 return name; 4748 } 4749 4750 4751 /** 4752 * Looks up the database based on the given URI. 4753 * 4754 * @param uri The requested URI 4755 * @returns the database for the given URI 4756 */ 4757 private DatabaseHelper getDatabaseForUri(Uri uri) { 4758 synchronized (mDatabases) { 4759 if (uri.getPathSegments().size() >= 1) { 4760 return mDatabases.get(uri.getPathSegments().get(0)); 4761 } 4762 } 4763 return null; 4764 } 4765 4766 static boolean isMediaDatabaseName(String name) { 4767 if (INTERNAL_DATABASE_NAME.equals(name)) { 4768 return true; 4769 } 4770 if (EXTERNAL_DATABASE_NAME.equals(name)) { 4771 return true; 4772 } 4773 if (name.startsWith("external-")) { 4774 return true; 4775 } 4776 return false; 4777 } 4778 4779 static boolean isInternalMediaDatabaseName(String name) { 4780 if (INTERNAL_DATABASE_NAME.equals(name)) { 4781 return true; 4782 } 4783 return false; 4784 } 4785 4786 /** 4787 * Attach the database for a volume (internal or external). 4788 * Does nothing if the volume is already attached, otherwise 4789 * checks the volume ID and sets up the corresponding database. 4790 * 4791 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}. 4792 * @return the content URI of the attached volume. 4793 */ 4794 private Uri attachVolume(String volume) { 4795 if (Binder.getCallingPid() != Process.myPid()) { 4796 throw new SecurityException( 4797 "Opening and closing databases not allowed."); 4798 } 4799 4800 synchronized (mDatabases) { 4801 if (mDatabases.get(volume) != null) { // Already attached 4802 return Uri.parse("content://media/" + volume); 4803 } 4804 4805 Context context = getContext(); 4806 DatabaseHelper helper; 4807 if (INTERNAL_VOLUME.equals(volume)) { 4808 helper = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true, 4809 false, mObjectRemovedCallback); 4810 } else if (EXTERNAL_VOLUME.equals(volume)) { 4811 if (Environment.isExternalStorageRemovable()) { 4812 String path = mExternalStoragePaths[0]; 4813 int volumeID = FileUtils.getFatVolumeId(path); 4814 if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID); 4815 4816 // generate database name based on volume ID 4817 String dbName = "external-" + Integer.toHexString(volumeID) + ".db"; 4818 helper = new DatabaseHelper(context, dbName, false, 4819 false, mObjectRemovedCallback); 4820 mVolumeId = volumeID; 4821 } else { 4822 // external database name should be EXTERNAL_DATABASE_NAME 4823 // however earlier releases used the external-XXXXXXXX.db naming 4824 // for devices without removable storage, and in that case we need to convert 4825 // to this new convention 4826 File dbFile = context.getDatabasePath(EXTERNAL_DATABASE_NAME); 4827 if (!dbFile.exists()) { 4828 // find the most recent external database and rename it to 4829 // EXTERNAL_DATABASE_NAME, and delete any other older 4830 // external database files 4831 File recentDbFile = null; 4832 for (String database : context.databaseList()) { 4833 if (database.startsWith("external-")) { 4834 File file = context.getDatabasePath(database); 4835 if (recentDbFile == null) { 4836 recentDbFile = file; 4837 } else if (file.lastModified() > recentDbFile.lastModified()) { 4838 recentDbFile.delete(); 4839 recentDbFile = file; 4840 } else { 4841 file.delete(); 4842 } 4843 } 4844 } 4845 if (recentDbFile != null) { 4846 if (recentDbFile.renameTo(dbFile)) { 4847 Log.d(TAG, "renamed database " + recentDbFile.getName() + 4848 " to " + EXTERNAL_DATABASE_NAME); 4849 } else { 4850 Log.e(TAG, "Failed to rename database " + recentDbFile.getName() + 4851 " to " + EXTERNAL_DATABASE_NAME); 4852 // This shouldn't happen, but if it does, continue using 4853 // the file under its old name 4854 dbFile = recentDbFile; 4855 } 4856 } 4857 // else DatabaseHelper will create one named EXTERNAL_DATABASE_NAME 4858 } 4859 helper = new DatabaseHelper(context, dbFile.getName(), false, 4860 false, mObjectRemovedCallback); 4861 } 4862 } else { 4863 throw new IllegalArgumentException("There is no volume named " + volume); 4864 } 4865 4866 mDatabases.put(volume, helper); 4867 4868 if (!helper.mInternal) { 4869 // create default directories (only happens on first boot) 4870 createDefaultFolders(helper, helper.getWritableDatabase()); 4871 4872 // clean up stray album art files: delete every file not in the database 4873 File[] files = new File(mExternalStoragePaths[0], ALBUM_THUMB_FOLDER).listFiles(); 4874 HashSet<String> fileSet = new HashSet(); 4875 for (int i = 0; files != null && i < files.length; i++) { 4876 fileSet.add(files[i].getPath()); 4877 } 4878 4879 Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, 4880 new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null); 4881 try { 4882 while (cursor != null && cursor.moveToNext()) { 4883 fileSet.remove(cursor.getString(0)); 4884 } 4885 } finally { 4886 if (cursor != null) cursor.close(); 4887 } 4888 4889 Iterator<String> iterator = fileSet.iterator(); 4890 while (iterator.hasNext()) { 4891 String filename = iterator.next(); 4892 if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename); 4893 new File(filename).delete(); 4894 } 4895 } 4896 } 4897 4898 if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume); 4899 return Uri.parse("content://media/" + volume); 4900 } 4901 4902 /** 4903 * Detach the database for a volume (must be external). 4904 * Does nothing if the volume is already detached, otherwise 4905 * closes the database and sends a notification to listeners. 4906 * 4907 * @param uri The content URI of the volume, as returned by {@link #attachVolume} 4908 */ 4909 private void detachVolume(Uri uri) { 4910 if (Binder.getCallingPid() != Process.myPid()) { 4911 throw new SecurityException( 4912 "Opening and closing databases not allowed."); 4913 } 4914 4915 String volume = uri.getPathSegments().get(0); 4916 if (INTERNAL_VOLUME.equals(volume)) { 4917 throw new UnsupportedOperationException( 4918 "Deleting the internal volume is not allowed"); 4919 } else if (!EXTERNAL_VOLUME.equals(volume)) { 4920 throw new IllegalArgumentException( 4921 "There is no volume named " + volume); 4922 } 4923 4924 synchronized (mDatabases) { 4925 DatabaseHelper database = mDatabases.get(volume); 4926 if (database == null) return; 4927 4928 try { 4929 // touch the database file to show it is most recently used 4930 File file = new File(database.getReadableDatabase().getPath()); 4931 file.setLastModified(System.currentTimeMillis()); 4932 } catch (Exception e) { 4933 Log.e(TAG, "Can't touch database file", e); 4934 } 4935 4936 mDatabases.remove(volume); 4937 database.close(); 4938 } 4939 4940 getContext().getContentResolver().notifyChange(uri, null); 4941 if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume); 4942 } 4943 4944 private static String TAG = "MediaProvider"; 4945 private static final boolean LOCAL_LOGV = false; 4946 4947 private static final String INTERNAL_DATABASE_NAME = "internal.db"; 4948 private static final String EXTERNAL_DATABASE_NAME = "external.db"; 4949 4950 // maximum number of cached external databases to keep 4951 private static final int MAX_EXTERNAL_DATABASES = 3; 4952 4953 // Delete databases that have not been used in two months 4954 // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60) 4955 private static final long OBSOLETE_DATABASE_DB = 5184000000L; 4956 4957 private HashMap<String, DatabaseHelper> mDatabases; 4958 4959 private Handler mThumbHandler; 4960 4961 // name of the volume currently being scanned by the media scanner (or null) 4962 private String mMediaScannerVolume; 4963 4964 // current FAT volume ID 4965 private int mVolumeId = -1; 4966 4967 static final String INTERNAL_VOLUME = "internal"; 4968 static final String EXTERNAL_VOLUME = "external"; 4969 static final String ALBUM_THUMB_FOLDER = "Android/data/com.android.providers.media/albumthumbs"; 4970 4971 // path for writing contents of in memory temp database 4972 private String mTempDatabasePath; 4973 4974 // WARNING: the values of IMAGES_MEDIA, AUDIO_MEDIA, and VIDEO_MEDIA and AUDIO_PLAYLISTS 4975 // are stored in the "files" table, so do not renumber them unless you also add 4976 // a corresponding database upgrade step for it. 4977 private static final int IMAGES_MEDIA = 1; 4978 private static final int IMAGES_MEDIA_ID = 2; 4979 private static final int IMAGES_THUMBNAILS = 3; 4980 private static final int IMAGES_THUMBNAILS_ID = 4; 4981 4982 private static final int AUDIO_MEDIA = 100; 4983 private static final int AUDIO_MEDIA_ID = 101; 4984 private static final int AUDIO_MEDIA_ID_GENRES = 102; 4985 private static final int AUDIO_MEDIA_ID_GENRES_ID = 103; 4986 private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104; 4987 private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105; 4988 private static final int AUDIO_GENRES = 106; 4989 private static final int AUDIO_GENRES_ID = 107; 4990 private static final int AUDIO_GENRES_ID_MEMBERS = 108; 4991 private static final int AUDIO_GENRES_ALL_MEMBERS = 109; 4992 private static final int AUDIO_PLAYLISTS = 110; 4993 private static final int AUDIO_PLAYLISTS_ID = 111; 4994 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 4995 private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113; 4996 private static final int AUDIO_ARTISTS = 114; 4997 private static final int AUDIO_ARTISTS_ID = 115; 4998 private static final int AUDIO_ALBUMS = 116; 4999 private static final int AUDIO_ALBUMS_ID = 117; 5000 private static final int AUDIO_ARTISTS_ID_ALBUMS = 118; 5001 private static final int AUDIO_ALBUMART = 119; 5002 private static final int AUDIO_ALBUMART_ID = 120; 5003 private static final int AUDIO_ALBUMART_FILE_ID = 121; 5004 5005 private static final int VIDEO_MEDIA = 200; 5006 private static final int VIDEO_MEDIA_ID = 201; 5007 private static final int VIDEO_THUMBNAILS = 202; 5008 private static final int VIDEO_THUMBNAILS_ID = 203; 5009 5010 private static final int VOLUMES = 300; 5011 private static final int VOLUMES_ID = 301; 5012 5013 private static final int AUDIO_SEARCH_LEGACY = 400; 5014 private static final int AUDIO_SEARCH_BASIC = 401; 5015 private static final int AUDIO_SEARCH_FANCY = 402; 5016 5017 private static final int MEDIA_SCANNER = 500; 5018 5019 private static final int FS_ID = 600; 5020 private static final int VERSION = 601; 5021 5022 private static final int FILES = 700; 5023 private static final int FILES_ID = 701; 5024 5025 // Used only by the MTP implementation 5026 private static final int MTP_OBJECTS = 702; 5027 private static final int MTP_OBJECTS_ID = 703; 5028 private static final int MTP_OBJECT_REFERENCES = 704; 5029 // UsbReceiver calls insert() and delete() with this URI to tell us 5030 // when MTP is connected and disconnected 5031 private static final int MTP_CONNECTED = 705; 5032 5033 private static final UriMatcher URI_MATCHER = 5034 new UriMatcher(UriMatcher.NO_MATCH); 5035 5036 private static final String[] ID_PROJECTION = new String[] { 5037 MediaStore.MediaColumns._ID 5038 }; 5039 5040 private static final String[] PATH_PROJECTION = new String[] { 5041 MediaStore.MediaColumns._ID, 5042 MediaStore.MediaColumns.DATA, 5043 }; 5044 5045 private static final String[] MIME_TYPE_PROJECTION = new String[] { 5046 MediaStore.MediaColumns._ID, // 0 5047 MediaStore.MediaColumns.MIME_TYPE, // 1 5048 }; 5049 5050 private static final String[] READY_FLAG_PROJECTION = new String[] { 5051 MediaStore.MediaColumns._ID, 5052 MediaStore.MediaColumns.DATA, 5053 Images.Media.MINI_THUMB_MAGIC 5054 }; 5055 5056 private static final String OBJECT_REFERENCES_QUERY = 5057 "SELECT " + Audio.Playlists.Members.AUDIO_ID + " FROM audio_playlists_map" 5058 + " WHERE " + Audio.Playlists.Members.PLAYLIST_ID + "=?" 5059 + " ORDER BY " + Audio.Playlists.Members.PLAY_ORDER; 5060 5061 static 5062 { 5063 URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA); 5064 URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID); 5065 URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS); 5066 URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID); 5067 5068 URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA); 5069 URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID); 5070 URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES); 5071 URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID); 5072 URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS); 5073 URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID); 5074 URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES); 5075 URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID); 5076 URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS); 5077 URI_MATCHER.addURI("media", "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS); 5078 URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS); 5079 URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID); 5080 URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS); 5081 URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID); 5082 URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS); 5083 URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID); 5084 URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS); 5085 URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS); 5086 URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID); 5087 URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART); 5088 URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID); 5089 URI_MATCHER.addURI("media", "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID); 5090 5091 URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA); 5092 URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID); 5093 URI_MATCHER.addURI("media", "*/video/thumbnails", VIDEO_THUMBNAILS); 5094 URI_MATCHER.addURI("media", "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID); 5095 5096 URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER); 5097 5098 URI_MATCHER.addURI("media", "*/fs_id", FS_ID); 5099 URI_MATCHER.addURI("media", "*/version", VERSION); 5100 5101 URI_MATCHER.addURI("media", "*/mtp_connected", MTP_CONNECTED); 5102 5103 URI_MATCHER.addURI("media", "*", VOLUMES_ID); 5104 URI_MATCHER.addURI("media", null, VOLUMES); 5105 5106 // Used by MTP implementation 5107 URI_MATCHER.addURI("media", "*/file", FILES); 5108 URI_MATCHER.addURI("media", "*/file/#", FILES_ID); 5109 URI_MATCHER.addURI("media", "*/object", MTP_OBJECTS); 5110 URI_MATCHER.addURI("media", "*/object/#", MTP_OBJECTS_ID); 5111 URI_MATCHER.addURI("media", "*/object/#/references", MTP_OBJECT_REFERENCES); 5112 5113 /** 5114 * @deprecated use the 'basic' or 'fancy' search Uris instead 5115 */ 5116 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY, 5117 AUDIO_SEARCH_LEGACY); 5118 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 5119 AUDIO_SEARCH_LEGACY); 5120 5121 // used for search suggestions 5122 URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY, 5123 AUDIO_SEARCH_BASIC); 5124 URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY + 5125 "/*", AUDIO_SEARCH_BASIC); 5126 5127 // used by the music app's search activity 5128 URI_MATCHER.addURI("media", "*/audio/search/fancy", AUDIO_SEARCH_FANCY); 5129 URI_MATCHER.addURI("media", "*/audio/search/fancy/*", AUDIO_SEARCH_FANCY); 5130 } 5131 5132 @Override 5133 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 5134 Collection<DatabaseHelper> foo = mDatabases.values(); 5135 for (DatabaseHelper dbh: foo) { 5136 StringBuilder s = new StringBuilder(); 5137 s.append(dbh.mName); 5138 s.append(": "); 5139 SQLiteDatabase db = dbh.getReadableDatabase(); 5140 if (db == null) { 5141 s.append("null"); 5142 } else { 5143 s.append("version " + db.getVersion() + ", "); 5144 Cursor c = db.query("files", new String[] {"count(*)"}, null, null, null, null, null); 5145 try { 5146 if (c != null && c.moveToFirst()) { 5147 int num = c.getInt(0); 5148 s.append(num + " rows, "); 5149 } else { 5150 s.append("couldn't get row count, "); 5151 } 5152 } finally { 5153 if (c != null) { 5154 c.close(); 5155 } 5156 } 5157 s.append(dbh.mNumInserts + " inserts, "); 5158 s.append(dbh.mNumUpdates + " updates, "); 5159 s.append(dbh.mNumDeletes + " deletes, "); 5160 s.append(dbh.mNumQueries + " queries, "); 5161 if (dbh.mScanStartTime != 0) { 5162 s.append("scan started " + DateUtils.formatDateTime(getContext(), 5163 dbh.mScanStartTime / 1000, 5164 DateUtils.FORMAT_SHOW_DATE 5165 | DateUtils.FORMAT_SHOW_TIME 5166 | DateUtils.FORMAT_ABBREV_ALL)); 5167 if (dbh.mScanStopTime < dbh.mScanStartTime) { 5168 s.append(" (ongoing)"); 5169 } else { 5170 s.append(" (" + DateUtils.formatElapsedTime( 5171 (dbh.mScanStopTime - dbh.mScanStartTime) / 1000000) + ")"); 5172 } 5173 } 5174 } 5175 writer.println(s); 5176 } 5177 if (mMediaScannerVolume != null) { 5178 writer.println("Scanning: " + mMediaScannerVolume); 5179 } 5180 } 5181} 5182