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