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