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