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