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