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