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