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