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