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