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