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