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