MediaProvider.java revision 044b029d9023f55e8e8861ed2f6e192f1b34f9bb
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.*; 21import android.database.AbstractCursor; 22import android.database.Cursor; 23import android.database.DatabaseUtils; 24import android.database.MatrixCursor; 25import android.database.SQLException; 26import android.database.sqlite.SQLiteDatabase; 27import android.database.sqlite.SQLiteOpenHelper; 28import android.database.sqlite.SQLiteQueryBuilder; 29import android.graphics.Bitmap; 30import android.graphics.BitmapFactory; 31import android.media.MediaScanner; 32import android.media.MiniThumbFile; 33import android.net.Uri; 34import android.os.Binder; 35import android.os.Environment; 36import android.os.FileUtils; 37import android.os.Handler; 38import android.os.Looper; 39import android.os.MemoryFile; 40import android.os.Message; 41import android.os.ParcelFileDescriptor; 42import android.os.Process; 43import android.provider.BaseColumns; 44import android.provider.MediaStore; 45import android.provider.MediaStore.Audio; 46import android.provider.MediaStore.Images; 47import android.provider.MediaStore.MediaColumns; 48import android.provider.MediaStore.Video; 49import android.provider.MediaStore.Images.ImageColumns; 50import android.text.TextUtils; 51import android.util.Log; 52 53import java.io.File; 54import java.io.FileInputStream; 55import java.io.FileNotFoundException; 56import java.io.IOException; 57import java.io.OutputStream; 58import java.text.Collator; 59import java.util.ArrayList; 60import java.util.HashMap; 61import java.util.HashSet; 62import java.util.Iterator; 63import java.util.List; 64import java.util.PriorityQueue; 65import java.util.Stack; 66 67/** 68 * Media content provider. See {@link android.provider.MediaStore} for details. 69 * Separate databases are kept for each external storage card we see (using the 70 * card's ID as an index). The content visible at content://media/external/... 71 * changes with the card. 72 */ 73public class MediaProvider extends ContentProvider { 74 private static final Uri MEDIA_URI = Uri.parse("content://media"); 75 private static final Uri ALBUMART_URI = Uri.parse("content://media/external/audio/albumart"); 76 private static final int ALBUM_THUMB = 1; 77 private static final int IMAGE_THUMB = 2; 78 79 private static final HashMap<String, String> sArtistAlbumsMap = new HashMap<String, String>(); 80 private static final HashMap<String, String> sFolderArtMap = new HashMap<String, String>(); 81 82 // A HashSet of paths that are pending creation of album art thumbnails. 83 private HashSet mPendingThumbs = new HashSet(); 84 85 // A Stack of outstanding thumbnail requests. 86 private Stack mThumbRequestStack = new Stack(); 87 88 // The lock of mMediaThumbQueue protects both mMediaThumbQueue and mCurrentThumbRequest. 89 private MediaThumbRequest mCurrentThumbRequest = null; 90 private PriorityQueue<MediaThumbRequest> mMediaThumbQueue = 91 new PriorityQueue<MediaThumbRequest>(MediaThumbRequest.PRIORITY_NORMAL, 92 MediaThumbRequest.getComparator()); 93 94 // For compatibility with the approximately 0 apps that used mediaprovider search in 95 // releases 1.0, 1.1 or 1.5 96 private String[] mSearchColsLegacy = new String[] { 97 android.provider.BaseColumns._ID, 98 MediaStore.Audio.Media.MIME_TYPE, 99 "(CASE WHEN grouporder=1 THEN " + R.drawable.ic_search_category_music_artist + 100 " ELSE CASE WHEN grouporder=2 THEN " + R.drawable.ic_search_category_music_album + 101 " ELSE " + R.drawable.ic_search_category_music_song + " END END" + 102 ") AS " + SearchManager.SUGGEST_COLUMN_ICON_1, 103 "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2, 104 "text1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 105 "text1 AS " + SearchManager.SUGGEST_COLUMN_QUERY, 106 "CASE when grouporder=1 THEN data1 ELSE artist END AS data1", 107 "CASE when grouporder=1 THEN data2 ELSE " + 108 "CASE WHEN grouporder=2 THEN NULL ELSE album END END AS data2", 109 "match as ar", 110 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 111 "grouporder", 112 "NULL AS itemorder" // We should be sorting by the artist/album/title keys, but that 113 // column is not available here, and the list is already sorted. 114 }; 115 private String[] mSearchColsFancy = new String[] { 116 android.provider.BaseColumns._ID, 117 MediaStore.Audio.Media.MIME_TYPE, 118 MediaStore.Audio.Artists.ARTIST, 119 MediaStore.Audio.Albums.ALBUM, 120 MediaStore.Audio.Media.TITLE, 121 "data1", 122 "data2", 123 }; 124 // If this array gets changed, please update the constant below to point to the correct item. 125 private String[] mSearchColsBasic = new String[] { 126 android.provider.BaseColumns._ID, 127 MediaStore.Audio.Media.MIME_TYPE, 128 "(CASE WHEN grouporder=1 THEN " + R.drawable.ic_search_category_music_artist + 129 " ELSE CASE WHEN grouporder=2 THEN " + R.drawable.ic_search_category_music_album + 130 " ELSE " + R.drawable.ic_search_category_music_song + " END END" + 131 ") AS " + SearchManager.SUGGEST_COLUMN_ICON_1, 132 "text1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 133 "text1 AS " + SearchManager.SUGGEST_COLUMN_QUERY, 134 "(CASE WHEN grouporder=1 THEN '%1'" + // %1 gets replaced with localized string. 135 " ELSE CASE WHEN grouporder=3 THEN artist || ' - ' || album" + 136 " ELSE CASE WHEN text2!='" + MediaStore.UNKNOWN_STRING + "' THEN text2" + 137 " ELSE NULL END END END) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2, 138 SearchManager.SUGGEST_COLUMN_INTENT_DATA 139 }; 140 // Position of the TEXT_2 item in the above array. 141 private final int SEARCH_COLUMN_BASIC_TEXT2 = 5; 142 143 private Uri mAlbumArtBaseUri = Uri.parse("content://media/external/audio/albumart"); 144 145 private BroadcastReceiver mUnmountReceiver = new BroadcastReceiver() { 146 @Override 147 public void onReceive(Context context, Intent intent) { 148 if (intent.getAction().equals(Intent.ACTION_MEDIA_EJECT)) { 149 // Remove the external volume and then notify all cursors backed by 150 // data on that volume 151 detachVolume(Uri.parse("content://media/external")); 152 sFolderArtMap.clear(); 153 MiniThumbFile.reset(); 154 } 155 } 156 }; 157 158 /** 159 * Wrapper class for a specific database (associated with one particular 160 * external card, or with internal storage). Can open the actual database 161 * on demand, create and upgrade the schema, etc. 162 */ 163 private static final class DatabaseHelper extends SQLiteOpenHelper { 164 final Context mContext; 165 final boolean mInternal; // True if this is the internal database 166 167 // In memory caches of artist and album data. 168 HashMap<String, Long> mArtistCache = new HashMap<String, Long>(); 169 HashMap<String, Long> mAlbumCache = new HashMap<String, Long>(); 170 171 public DatabaseHelper(Context context, String name, boolean internal) { 172 super(context, name, null, DATABASE_VERSION); 173 mContext = context; 174 mInternal = internal; 175 } 176 177 /** 178 * Creates database the first time we try to open it. 179 */ 180 @Override 181 public void onCreate(final SQLiteDatabase db) { 182 updateDatabase(db, mInternal, 0, DATABASE_VERSION); 183 } 184 185 /** 186 * Updates the database format when a new content provider is used 187 * with an older database format. 188 */ 189 @Override 190 public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) { 191 updateDatabase(db, mInternal, oldV, newV); 192 } 193 194 /** 195 * Touch this particular database and garbage collect old databases. 196 * An LRU cache system is used to clean up databases for old external 197 * storage volumes. 198 */ 199 @Override 200 public void onOpen(SQLiteDatabase db) { 201 if (mInternal) return; // The internal database is kept separately. 202 203 // touch the database file to show it is most recently used 204 File file = new File(db.getPath()); 205 long now = System.currentTimeMillis(); 206 file.setLastModified(now); 207 208 // delete least recently used databases if we are over the limit 209 String[] databases = mContext.databaseList(); 210 int count = databases.length; 211 int limit = MAX_EXTERNAL_DATABASES; 212 213 // delete external databases that have not been used in the past two months 214 long twoMonthsAgo = now - OBSOLETE_DATABASE_DB; 215 for (int i = 0; i < databases.length; i++) { 216 File other = mContext.getDatabasePath(databases[i]); 217 if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) { 218 databases[i] = null; 219 count--; 220 if (file.equals(other)) { 221 // reduce limit to account for the existence of the database we 222 // are about to open, which we removed from the list. 223 limit--; 224 } 225 } else { 226 long time = other.lastModified(); 227 if (time < twoMonthsAgo) { 228 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[i]); 229 mContext.deleteDatabase(databases[i]); 230 databases[i] = null; 231 count--; 232 } 233 } 234 } 235 236 // delete least recently used databases until 237 // we are no longer over the limit 238 while (count > limit) { 239 int lruIndex = -1; 240 long lruTime = 0; 241 242 for (int i = 0; i < databases.length; i++) { 243 if (databases[i] != null) { 244 long time = mContext.getDatabasePath(databases[i]).lastModified(); 245 if (lruTime == 0 || time < lruTime) { 246 lruIndex = i; 247 lruTime = time; 248 } 249 } 250 } 251 252 // delete least recently used database 253 if (lruIndex != -1) { 254 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]); 255 mContext.deleteDatabase(databases[lruIndex]); 256 databases[lruIndex] = null; 257 count--; 258 } 259 } 260 } 261 } 262 263 @Override 264 public boolean onCreate() { 265 sArtistAlbumsMap.put(MediaStore.Audio.Albums._ID, "audio.album_id AS " + 266 MediaStore.Audio.Albums._ID); 267 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM, "album"); 268 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_KEY, "album_key"); 269 sArtistAlbumsMap.put(MediaStore.Audio.Albums.FIRST_YEAR, "MIN(year) AS " + 270 MediaStore.Audio.Albums.FIRST_YEAR); 271 sArtistAlbumsMap.put(MediaStore.Audio.Albums.LAST_YEAR, "MAX(year) AS " + 272 MediaStore.Audio.Albums.LAST_YEAR); 273 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST, "artist"); 274 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_ID, "artist"); 275 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_KEY, "artist_key"); 276 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS, "count(*) AS " + 277 MediaStore.Audio.Albums.NUMBER_OF_SONGS); 278 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_ART, "album_art._data AS " + 279 MediaStore.Audio.Albums.ALBUM_ART); 280 281 mSearchColsBasic[SEARCH_COLUMN_BASIC_TEXT2] = 282 mSearchColsBasic[SEARCH_COLUMN_BASIC_TEXT2].replaceAll( 283 "%1", getContext().getString(R.string.artist_label)); 284 mDatabases = new HashMap<String, DatabaseHelper>(); 285 attachVolume(INTERNAL_VOLUME); 286 287 IntentFilter iFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); 288 iFilter.addDataScheme("file"); 289 getContext().registerReceiver(mUnmountReceiver, iFilter); 290 291 // open external database if external storage is mounted 292 String state = Environment.getExternalStorageState(); 293 if (Environment.MEDIA_MOUNTED.equals(state) || 294 Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 295 attachVolume(EXTERNAL_VOLUME); 296 } 297 298 mThumbWorker = new Worker("thumbs thread"); 299 mThumbHandler = new Handler(mThumbWorker.getLooper()) { 300 @Override 301 public void handleMessage(Message msg) { 302 if (msg.what == IMAGE_THUMB) { 303 synchronized (mMediaThumbQueue) { 304 mCurrentThumbRequest = mMediaThumbQueue.poll(); 305 } 306 if (mCurrentThumbRequest == null) { 307 Log.w(TAG, "Have message but no request?"); 308 } else { 309 try { 310 File origFile = new File(mCurrentThumbRequest.mPath); 311 if (origFile.exists() && origFile.length() > 0) { 312 mCurrentThumbRequest.execute(); 313 } else { 314 // original file hasn't been stored yet 315 synchronized (mMediaThumbQueue) { 316 Log.w(TAG, "original file hasn't been stored yet: " + mCurrentThumbRequest.mPath); 317 } 318 } 319 } catch (IOException ex) { 320 Log.w(TAG, ex); 321 } catch (UnsupportedOperationException ex) { 322 // This could happen if we unplug the sd card during insert/update/delete 323 // See getDatabaseForUri. 324 Log.w(TAG, ex); 325 } finally { 326 synchronized (mCurrentThumbRequest) { 327 mCurrentThumbRequest.mState = MediaThumbRequest.State.DONE; 328 mCurrentThumbRequest.notifyAll(); 329 } 330 } 331 } 332 } else if (msg.what == ALBUM_THUMB) { 333 ThumbData d; 334 synchronized (mThumbRequestStack) { 335 d = (ThumbData)mThumbRequestStack.pop(); 336 } 337 338 makeThumbInternal(d); 339 synchronized (mPendingThumbs) { 340 mPendingThumbs.remove(d.path); 341 } 342 } 343 } 344 }; 345 346 return true; 347 } 348 349 /** 350 * This method takes care of updating all the tables in the database to the 351 * current version, creating them if necessary. 352 * This method can only update databases at schema 63 or higher, which was 353 * created August 1, 2008. Older database will be cleared and recreated. 354 * @param db Database 355 * @param internal True if this is the internal media database 356 */ 357 private static void updateDatabase(SQLiteDatabase db, boolean internal, 358 int fromVersion, int toVersion) { 359 360 // sanity checks 361 if (toVersion != DATABASE_VERSION) { 362 Log.e(TAG, "Illegal update request. Got " + toVersion + ", expected " + 363 DATABASE_VERSION); 364 throw new IllegalArgumentException(); 365 } else if (fromVersion > toVersion) { 366 Log.e(TAG, "Illegal update request: can't downgrade from " + fromVersion + 367 " to " + toVersion + ". Did you forget to wipe data?"); 368 throw new IllegalArgumentException(); 369 } 370 371 if (fromVersion < 63) { 372 // Drop everything and start over. 373 Log.i(TAG, "Upgrading media database from version " + 374 fromVersion + " to " + toVersion + ", which will destroy all old data"); 375 db.execSQL("DROP TABLE IF EXISTS images"); 376 db.execSQL("DROP TRIGGER IF EXISTS images_cleanup"); 377 db.execSQL("DROP TABLE IF EXISTS thumbnails"); 378 db.execSQL("DROP TRIGGER IF EXISTS thumbnails_cleanup"); 379 db.execSQL("DROP TABLE IF EXISTS audio_meta"); 380 db.execSQL("DROP TABLE IF EXISTS artists"); 381 db.execSQL("DROP TABLE IF EXISTS albums"); 382 db.execSQL("DROP TABLE IF EXISTS album_art"); 383 db.execSQL("DROP VIEW IF EXISTS artist_info"); 384 db.execSQL("DROP VIEW IF EXISTS album_info"); 385 db.execSQL("DROP VIEW IF EXISTS artists_albums_map"); 386 db.execSQL("DROP TRIGGER IF EXISTS audio_meta_cleanup"); 387 db.execSQL("DROP TABLE IF EXISTS audio_genres"); 388 db.execSQL("DROP TABLE IF EXISTS audio_genres_map"); 389 db.execSQL("DROP TRIGGER IF EXISTS audio_genres_cleanup"); 390 db.execSQL("DROP TABLE IF EXISTS audio_playlists"); 391 db.execSQL("DROP TABLE IF EXISTS audio_playlists_map"); 392 db.execSQL("DROP TRIGGER IF EXISTS audio_playlists_cleanup"); 393 db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup1"); 394 db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup2"); 395 db.execSQL("DROP TABLE IF EXISTS video"); 396 db.execSQL("DROP TRIGGER IF EXISTS video_cleanup"); 397 398 db.execSQL("CREATE TABLE IF NOT EXISTS images (" + 399 "_id INTEGER PRIMARY KEY," + 400 "_data TEXT," + 401 "_size INTEGER," + 402 "_display_name TEXT," + 403 "mime_type TEXT," + 404 "title TEXT," + 405 "date_added INTEGER," + 406 "date_modified INTEGER," + 407 "description TEXT," + 408 "picasa_id TEXT," + 409 "isprivate INTEGER," + 410 "latitude DOUBLE," + 411 "longitude DOUBLE," + 412 "datetaken INTEGER," + 413 "orientation INTEGER," + 414 "mini_thumb_magic INTEGER," + 415 "bucket_id TEXT," + 416 "bucket_display_name TEXT" + 417 ");"); 418 419 db.execSQL("CREATE INDEX IF NOT EXISTS mini_thumb_magic_index on images(mini_thumb_magic);"); 420 421 db.execSQL("CREATE TRIGGER IF NOT EXISTS images_cleanup DELETE ON images " + 422 "BEGIN " + 423 "DELETE FROM thumbnails WHERE image_id = old._id;" + 424 "SELECT _DELETE_FILE(old._data);" + 425 "END"); 426 427 // create image thumbnail table 428 db.execSQL("CREATE TABLE IF NOT EXISTS thumbnails (" + 429 "_id INTEGER PRIMARY KEY," + 430 "_data TEXT," + 431 "image_id INTEGER," + 432 "kind INTEGER," + 433 "width INTEGER," + 434 "height INTEGER" + 435 ");"); 436 437 db.execSQL("CREATE INDEX IF NOT EXISTS image_id_index on thumbnails(image_id);"); 438 439 db.execSQL("CREATE TRIGGER IF NOT EXISTS thumbnails_cleanup DELETE ON thumbnails " + 440 "BEGIN " + 441 "SELECT _DELETE_FILE(old._data);" + 442 "END"); 443 444 // Contains meta data about audio files 445 db.execSQL("CREATE TABLE IF NOT EXISTS audio_meta (" + 446 "_id INTEGER PRIMARY KEY," + 447 "_data TEXT NOT NULL," + 448 "_display_name TEXT," + 449 "_size INTEGER," + 450 "mime_type TEXT," + 451 "date_added INTEGER," + 452 "date_modified INTEGER," + 453 "title TEXT NOT NULL," + 454 "title_key TEXT NOT NULL," + 455 "duration INTEGER," + 456 "artist_id INTEGER," + 457 "composer TEXT," + 458 "album_id INTEGER," + 459 "track INTEGER," + // track is an integer to allow proper sorting 460 "year INTEGER CHECK(year!=0)," + 461 "is_ringtone INTEGER," + 462 "is_music INTEGER," + 463 "is_alarm INTEGER," + 464 "is_notification INTEGER" + 465 ");"); 466 467 // Contains a sort/group "key" and the preferred display name for artists 468 db.execSQL("CREATE TABLE IF NOT EXISTS artists (" + 469 "artist_id INTEGER PRIMARY KEY," + 470 "artist_key TEXT NOT NULL UNIQUE," + 471 "artist TEXT NOT NULL" + 472 ");"); 473 474 // Contains a sort/group "key" and the preferred display name for albums 475 db.execSQL("CREATE TABLE IF NOT EXISTS albums (" + 476 "album_id INTEGER PRIMARY KEY," + 477 "album_key TEXT NOT NULL UNIQUE," + 478 "album TEXT NOT NULL" + 479 ");"); 480 481 db.execSQL("CREATE TABLE IF NOT EXISTS album_art (" + 482 "album_id INTEGER PRIMARY KEY," + 483 "_data TEXT" + 484 ");"); 485 486 recreateAudioView(db); 487 488 489 // Provides some extra info about artists, like the number of tracks 490 // and albums for this artist 491 db.execSQL("CREATE VIEW IF NOT EXISTS artist_info AS " + 492 "SELECT artist_id AS _id, artist, artist_key, " + 493 "COUNT(DISTINCT album) AS number_of_albums, " + 494 "COUNT(*) AS number_of_tracks FROM audio WHERE is_music=1 "+ 495 "GROUP BY artist_key;"); 496 497 // Provides extra info albums, such as the number of tracks 498 db.execSQL("CREATE VIEW IF NOT EXISTS album_info AS " + 499 "SELECT audio.album_id AS _id, album, album_key, " + 500 "MIN(year) AS minyear, " + 501 "MAX(year) AS maxyear, artist, artist_id, artist_key, " + 502 "count(*) AS " + MediaStore.Audio.Albums.NUMBER_OF_SONGS + 503 ",album_art._data AS album_art" + 504 " FROM audio LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id" + 505 " WHERE is_music=1 GROUP BY audio.album_id;"); 506 507 // For a given artist_id, provides the album_id for albums on 508 // which the artist appears. 509 db.execSQL("CREATE VIEW IF NOT EXISTS artists_albums_map AS " + 510 "SELECT DISTINCT artist_id, album_id FROM audio_meta;"); 511 512 /* 513 * Only external media volumes can handle genres, playlists, etc. 514 */ 515 if (!internal) { 516 // Cleans up when an audio file is deleted 517 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_meta_cleanup DELETE ON audio_meta " + 518 "BEGIN " + 519 "DELETE FROM audio_genres_map WHERE audio_id = old._id;" + 520 "DELETE FROM audio_playlists_map WHERE audio_id = old._id;" + 521 "END"); 522 523 // Contains audio genre definitions 524 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres (" + 525 "_id INTEGER PRIMARY KEY," + 526 "name TEXT NOT NULL" + 527 ");"); 528 529 // Contiains mappings between audio genres and audio files 530 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres_map (" + 531 "_id INTEGER PRIMARY KEY," + 532 "audio_id INTEGER NOT NULL," + 533 "genre_id INTEGER NOT NULL" + 534 ");"); 535 536 // Cleans up when an audio genre is delete 537 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_genres_cleanup DELETE ON audio_genres " + 538 "BEGIN " + 539 "DELETE FROM audio_genres_map WHERE genre_id = old._id;" + 540 "END"); 541 542 // Contains audio playlist definitions 543 db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists (" + 544 "_id INTEGER PRIMARY KEY," + 545 "_data TEXT," + // _data is path for file based playlists, or null 546 "name TEXT NOT NULL," + 547 "date_added INTEGER," + 548 "date_modified INTEGER" + 549 ");"); 550 551 // Contains mappings between audio playlists and audio files 552 db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists_map (" + 553 "_id INTEGER PRIMARY KEY," + 554 "audio_id INTEGER NOT NULL," + 555 "playlist_id INTEGER NOT NULL," + 556 "play_order INTEGER NOT NULL" + 557 ");"); 558 559 // Cleans up when an audio playlist is deleted 560 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_playlists_cleanup DELETE ON audio_playlists " + 561 "BEGIN " + 562 "DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" + 563 "SELECT _DELETE_FILE(old._data);" + 564 "END"); 565 566 // Cleans up album_art table entry when an album is deleted 567 db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup1 DELETE ON albums " + 568 "BEGIN " + 569 "DELETE FROM album_art WHERE album_id = old.album_id;" + 570 "END"); 571 572 // Cleans up album_art when an album is deleted 573 db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup2 DELETE ON album_art " + 574 "BEGIN " + 575 "SELECT _DELETE_FILE(old._data);" + 576 "END"); 577 } 578 579 // Contains meta data about video files 580 db.execSQL("CREATE TABLE IF NOT EXISTS video (" + 581 "_id INTEGER PRIMARY KEY," + 582 "_data TEXT NOT NULL," + 583 "_display_name TEXT," + 584 "_size INTEGER," + 585 "mime_type TEXT," + 586 "date_added INTEGER," + 587 "date_modified INTEGER," + 588 "title TEXT," + 589 "duration INTEGER," + 590 "artist TEXT," + 591 "album TEXT," + 592 "resolution TEXT," + 593 "description TEXT," + 594 "isprivate INTEGER," + // for YouTube videos 595 "tags TEXT," + // for YouTube videos 596 "category TEXT," + // for YouTube videos 597 "language TEXT," + // for YouTube videos 598 "mini_thumb_data TEXT," + 599 "latitude DOUBLE," + 600 "longitude DOUBLE," + 601 "datetaken INTEGER," + 602 "mini_thumb_magic INTEGER" + 603 ");"); 604 605 db.execSQL("CREATE TRIGGER IF NOT EXISTS video_cleanup DELETE ON video " + 606 "BEGIN " + 607 "SELECT _DELETE_FILE(old._data);" + 608 "END"); 609 } 610 611 // At this point the database is at least at schema version 63 (it was 612 // either created at version 63 by the code above, or was already at 613 // version 63 or later) 614 615 if (fromVersion < 64) { 616 // create the index that updates the database to schema version 64 617 db.execSQL("CREATE INDEX IF NOT EXISTS sort_index on images(datetaken ASC, _id ASC);"); 618 } 619 620 if (fromVersion < 65) { 621 // create the index that updates the database to schema version 65 622 db.execSQL("CREATE INDEX IF NOT EXISTS titlekey_index on audio_meta(title_key);"); 623 } 624 625 if (fromVersion < 66) { 626 updateBucketNames(db, "images"); 627 } 628 629 if (fromVersion < 67) { 630 // create the indices that update the database to schema version 67 631 db.execSQL("CREATE INDEX IF NOT EXISTS albumkey_index on albums(album_key);"); 632 db.execSQL("CREATE INDEX IF NOT EXISTS artistkey_index on artists(artist_key);"); 633 } 634 635 if (fromVersion < 68) { 636 // Create bucket_id and bucket_display_name columns for the video table. 637 db.execSQL("ALTER TABLE video ADD COLUMN bucket_id TEXT;"); 638 db.execSQL("ALTER TABLE video ADD COLUMN bucket_display_name TEXT"); 639 updateBucketNames(db, "video"); 640 } 641 642 if (fromVersion < 69) { 643 updateDisplayName(db, "images"); 644 } 645 646 if (fromVersion < 70) { 647 // Create bookmark column for the video table. 648 db.execSQL("ALTER TABLE video ADD COLUMN bookmark INTEGER;"); 649 } 650 651 if (fromVersion < 71) { 652 // There is no change to the database schema, however a code change 653 // fixed parsing of metadata for certain files bought from the 654 // iTunes music store, so we want to rescan files that might need it. 655 // We do this by clearing the modification date in the database for 656 // those files, so that the media scanner will see them as updated 657 // and rescan them. 658 db.execSQL("UPDATE audio_meta SET date_modified=0 WHERE _id IN (" + 659 "SELECT _id FROM audio where mime_type='audio/mp4' AND " + 660 "artist='" + MediaStore.UNKNOWN_STRING + "' AND " + 661 "album='" + MediaStore.UNKNOWN_STRING + "'" + 662 ");"); 663 } 664 665 if (fromVersion < 72) { 666 // Create is_podcast and bookmark columns for the audio table. 667 db.execSQL("ALTER TABLE audio_meta ADD COLUMN is_podcast INTEGER;"); 668 db.execSQL("UPDATE audio_meta SET is_podcast=1 WHERE _data LIKE '%/podcasts/%';"); 669 db.execSQL("UPDATE audio_meta SET is_music=0 WHERE is_podcast=1" + 670 " AND _data NOT LIKE '%/music/%';"); 671 db.execSQL("ALTER TABLE audio_meta ADD COLUMN bookmark INTEGER;"); 672 673 // New columns added to tables aren't visible in views on those tables 674 // without opening and closing the database (or using the 'vacuum' command, 675 // which we can't do here because all this code runs inside a transaction). 676 // To work around this, we drop and recreate the affected view and trigger. 677 recreateAudioView(db); 678 } 679 680 if (fromVersion < 73) { 681 // There is no change to the database schema, but we now do case insensitive 682 // matching of folder names when determining whether something is music, a 683 // ringtone, podcast, etc, so we might need to reclassify some files. 684 db.execSQL("UPDATE audio_meta SET is_music=1 WHERE is_music=0 AND " + 685 "_data LIKE '%/music/%';"); 686 db.execSQL("UPDATE audio_meta SET is_ringtone=1 WHERE is_ringtone=0 AND " + 687 "_data LIKE '%/ringtones/%';"); 688 db.execSQL("UPDATE audio_meta SET is_notification=1 WHERE is_notification=0 AND " + 689 "_data LIKE '%/notifications/%';"); 690 db.execSQL("UPDATE audio_meta SET is_alarm=1 WHERE is_alarm=0 AND " + 691 "_data LIKE '%/alarms/%';"); 692 db.execSQL("UPDATE audio_meta SET is_podcast=1 WHERE is_podcast=0 AND " + 693 "_data LIKE '%/podcasts/%';"); 694 } 695 696 if (fromVersion < 74) { 697 // This view is used instead of the audio view by the union below, to force 698 // sqlite to use the title_key index. This greatly reduces memory usage 699 // (no separate copy pass needed for sorting, which could cause errors on 700 // large datasets) and improves speed (by about 35% on a large dataset) 701 db.execSQL("CREATE VIEW IF NOT EXISTS searchhelpertitle AS SELECT * FROM audio " + 702 "ORDER BY title_key;"); 703 704 db.execSQL("CREATE VIEW IF NOT EXISTS search AS " + 705 "SELECT _id," + 706 "'artist' AS mime_type," + 707 "artist," + 708 "NULL AS album," + 709 "NULL AS title," + 710 "artist AS text1," + 711 "NULL AS text2," + 712 "number_of_albums AS data1," + 713 "number_of_tracks AS data2," + 714 "artist_key AS match," + 715 "'content://media/external/audio/artists/'||_id AS suggest_intent_data," + 716 "1 AS grouporder " + 717 "FROM artist_info WHERE (artist!='" + MediaStore.UNKNOWN_STRING + "') " + 718 "UNION ALL " + 719 "SELECT _id," + 720 "'album' AS mime_type," + 721 "artist," + 722 "album," + 723 "NULL AS title," + 724 "album AS text1," + 725 "artist AS text2," + 726 "NULL AS data1," + 727 "NULL AS data2," + 728 "artist_key||' '||album_key AS match," + 729 "'content://media/external/audio/albums/'||_id AS suggest_intent_data," + 730 "2 AS grouporder " + 731 "FROM album_info WHERE (album!='" + MediaStore.UNKNOWN_STRING + "') " + 732 "UNION ALL " + 733 "SELECT searchhelpertitle._id AS _id," + 734 "mime_type," + 735 "artist," + 736 "album," + 737 "title," + 738 "title AS text1," + 739 "artist AS text2," + 740 "NULL AS data1," + 741 "NULL AS data2," + 742 "artist_key||' '||album_key||' '||title_key AS match," + 743 "'content://media/external/audio/media/'||searchhelpertitle._id AS " + 744 "suggest_intent_data," + 745 "3 AS grouporder " + 746 "FROM searchhelpertitle WHERE (title != '') " 747 ); 748 } 749 750 if (fromVersion < 75) { 751 // Force a rescan of the audio entries so we can apply the new logic to 752 // distinguish same-named albums. 753 db.execSQL("UPDATE audio_meta SET date_modified=0;"); 754 db.execSQL("DELETE FROM albums"); 755 } 756 757 if (fromVersion < 76) { 758 // We now ignore double quotes when building the key, so we have to remove all of them 759 // from existing keys. 760 db.execSQL("UPDATE audio_meta SET title_key=" + 761 "REPLACE(title_key,x'081D08C29F081D',x'081D') " + 762 "WHERE title_key LIKE '%'||x'081D08C29F081D'||'%';"); 763 db.execSQL("UPDATE albums SET album_key=" + 764 "REPLACE(album_key,x'081D08C29F081D',x'081D') " + 765 "WHERE album_key LIKE '%'||x'081D08C29F081D'||'%';"); 766 db.execSQL("UPDATE artists SET artist_key=" + 767 "REPLACE(artist_key,x'081D08C29F081D',x'081D') " + 768 "WHERE artist_key LIKE '%'||x'081D08C29F081D'||'%';"); 769 } 770 771 if (fromVersion < 77) { 772 // create video thumbnail table 773 db.execSQL("CREATE TABLE IF NOT EXISTS videothumbnails (" + 774 "_id INTEGER PRIMARY KEY," + 775 "_data TEXT," + 776 "video_id INTEGER," + 777 "kind INTEGER," + 778 "width INTEGER," + 779 "height INTEGER" + 780 ");"); 781 782 db.execSQL("CREATE INDEX IF NOT EXISTS video_id_index on videothumbnails(video_id);"); 783 784 db.execSQL("CREATE TRIGGER IF NOT EXISTS videothumbnails_cleanup DELETE ON videothumbnails " + 785 "BEGIN " + 786 "SELECT _DELETE_FILE(old._data);" + 787 "END"); 788 } 789 790 if (fromVersion < 78) { 791 // Force a rescan of the video entries so we can update 792 // latest changed DATE_TAKEN units (in milliseconds). 793 db.execSQL("UPDATE video SET date_modified=0;"); 794 } 795 796 if (fromVersion < 79) { 797 // move /sdcard/albumthumbs to 798 // /sdcard/Android/data/com.android.providers.media/albumthumbs, 799 // and update the database accordingly 800 801 String storageroot = Environment.getExternalStorageDirectory().getAbsolutePath(); 802 String oldthumbspath = storageroot + "/albumthumbs"; 803 String newthumbspath = storageroot + "/" + ALBUM_THUMB_FOLDER; 804 File thumbsfolder = new File(oldthumbspath); 805 if (thumbsfolder.exists()) { 806 // move folder to its new location 807 File newthumbsfolder = new File(newthumbspath); 808 newthumbsfolder.getParentFile().mkdirs(); 809 if(thumbsfolder.renameTo(newthumbsfolder)) { 810 // update the database 811 db.execSQL("UPDATE album_art SET _data=REPLACE(_data, '" + 812 oldthumbspath + "','" + newthumbspath + "');"); 813 } 814 } 815 } 816 817 if (fromVersion < 80) { 818 // Force rescan of image entries to update DATE_TAKEN as UTC timestamp. 819 db.execSQL("UPDATE images SET date_modified=0;"); 820 } 821 } 822 823 private static void recreateAudioView(SQLiteDatabase db) { 824 // Provides a unified audio/artist/album info view. 825 // Note that views are read-only, so we define a trigger to allow deletes. 826 db.execSQL("DROP VIEW IF EXISTS audio"); 827 db.execSQL("DROP TRIGGER IF EXISTS audio_delete"); 828 db.execSQL("CREATE VIEW IF NOT EXISTS audio as SELECT * FROM audio_meta " + 829 "LEFT OUTER JOIN artists ON audio_meta.artist_id=artists.artist_id " + 830 "LEFT OUTER JOIN albums ON audio_meta.album_id=albums.album_id;"); 831 832 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_delete INSTEAD OF DELETE ON audio " + 833 "BEGIN " + 834 "DELETE from audio_meta where _id=old._id;" + 835 "DELETE from audio_playlists_map where audio_id=old._id;" + 836 "DELETE from audio_genres_map where audio_id=old._id;" + 837 "END"); 838 } 839 840 /** 841 * Iterate through the rows of a table in a database, ensuring that the bucket_id and 842 * bucket_display_name columns are correct. 843 * @param db 844 * @param tableName 845 */ 846 private static void updateBucketNames(SQLiteDatabase db, String tableName) { 847 // Rebuild the bucket_display_name column using the natural case rather than lower case. 848 db.beginTransaction(); 849 try { 850 String[] columns = {BaseColumns._ID, MediaColumns.DATA}; 851 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 852 try { 853 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 854 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 855 while (cursor.moveToNext()) { 856 String data = cursor.getString(dataColumnIndex); 857 ContentValues values = new ContentValues(); 858 computeBucketValues(data, values); 859 int rowId = cursor.getInt(idColumnIndex); 860 db.update(tableName, values, "_id=" + rowId, null); 861 } 862 } finally { 863 cursor.close(); 864 } 865 db.setTransactionSuccessful(); 866 } finally { 867 db.endTransaction(); 868 } 869 } 870 871 /** 872 * Iterate through the rows of a table in a database, ensuring that the 873 * display name column has a value. 874 * @param db 875 * @param tableName 876 */ 877 private static void updateDisplayName(SQLiteDatabase db, String tableName) { 878 // Fill in default values for null displayName values 879 db.beginTransaction(); 880 try { 881 String[] columns = {BaseColumns._ID, MediaColumns.DATA, MediaColumns.DISPLAY_NAME}; 882 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 883 try { 884 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 885 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 886 final int displayNameIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME); 887 ContentValues values = new ContentValues(); 888 while (cursor.moveToNext()) { 889 String displayName = cursor.getString(displayNameIndex); 890 if (displayName == null) { 891 String data = cursor.getString(dataColumnIndex); 892 values.clear(); 893 computeDisplayName(data, values); 894 int rowId = cursor.getInt(idColumnIndex); 895 db.update(tableName, values, "_id=" + rowId, null); 896 } 897 } 898 } finally { 899 cursor.close(); 900 } 901 db.setTransactionSuccessful(); 902 } finally { 903 db.endTransaction(); 904 } 905 } 906 /** 907 * @param data The input path 908 * @param values the content values, where the bucked id name and bucket display name are updated. 909 * 910 */ 911 912 private static void computeBucketValues(String data, ContentValues values) { 913 File parentFile = new File(data).getParentFile(); 914 if (parentFile == null) { 915 parentFile = new File("/"); 916 } 917 918 // Lowercase the path for hashing. This avoids duplicate buckets if the 919 // filepath case is changed externally. 920 // Keep the original case for display. 921 String path = parentFile.toString().toLowerCase(); 922 String name = parentFile.getName(); 923 924 // Note: the BUCKET_ID and BUCKET_DISPLAY_NAME attributes are spelled the 925 // same for both images and video. However, for backwards-compatibility reasons 926 // there is no common base class. We use the ImageColumns version here 927 values.put(ImageColumns.BUCKET_ID, path.hashCode()); 928 values.put(ImageColumns.BUCKET_DISPLAY_NAME, name); 929 } 930 931 /** 932 * @param data The input path 933 * @param values the content values, where the display name is updated. 934 * 935 */ 936 private static void computeDisplayName(String data, ContentValues values) { 937 String s = (data == null ? "" : data.toString()); 938 int idx = s.lastIndexOf('/'); 939 if (idx >= 0) { 940 s = s.substring(idx + 1); 941 } 942 values.put("_display_name", s); 943 } 944 945 /** 946 * This method blocks until thumbnail is ready. 947 * 948 * @param thumbUri 949 * @return 950 */ 951 private boolean waitForThumbnailReady(Uri origUri) { 952 Cursor c = this.query(origUri, new String[] { ImageColumns._ID, ImageColumns.DATA, 953 ImageColumns.MINI_THUMB_MAGIC}, null, null, null); 954 if (c == null) return false; 955 956 boolean result = false; 957 958 if (c.moveToFirst()) { 959 long id = c.getLong(0); 960 String path = c.getString(1); 961 long magic = c.getLong(2); 962 963 MediaThumbRequest req = requestMediaThumbnail(path, origUri, 964 MediaThumbRequest.PRIORITY_HIGH, magic); 965 if (req == null) { 966 return false; 967 } 968 synchronized (req) { 969 try { 970 while (req.mState == MediaThumbRequest.State.WAIT) { 971 req.wait(); 972 } 973 } catch (InterruptedException e) { 974 Log.w(TAG, e); 975 } 976 if (req.mState == MediaThumbRequest.State.DONE) { 977 result = true; 978 } 979 } 980 } 981 c.close(); 982 983 return result; 984 } 985 986 private boolean matchThumbRequest(MediaThumbRequest req, int pid, long id, long gid, 987 boolean isVideo) { 988 boolean cancelAllOrigId = (id == -1); 989 boolean cancelAllGroupId = (gid == -1); 990 return (req.mCallingPid == pid) && 991 (cancelAllGroupId || req.mGroupId == gid) && 992 (cancelAllOrigId || req.mOrigId == id) && 993 (req.mIsVideo == isVideo); 994 } 995 996 private boolean queryThumbnail(SQLiteQueryBuilder qb, Uri uri, String table, 997 String column, boolean hasThumbnailId) { 998 qb.setTables(table); 999 if (hasThumbnailId) { 1000 // For uri dispatched to this method, the 4th path segment is always 1001 // the thumbnail id. 1002 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 1003 // client already knows which thumbnail it wants, bypass it. 1004 return true; 1005 } 1006 String origId = uri.getQueryParameter("orig_id"); 1007 // We can't query ready_flag unless we know original id 1008 if (origId == null) { 1009 // this could be thumbnail query for other purpose, bypass it. 1010 return true; 1011 } 1012 1013 boolean needBlocking = "1".equals(uri.getQueryParameter("blocking")); 1014 boolean cancelRequest = "1".equals(uri.getQueryParameter("cancel")); 1015 Uri origUri = uri.buildUpon().encodedPath( 1016 uri.getPath().replaceFirst("thumbnails", "media")) 1017 .appendPath(origId).build(); 1018 1019 if (needBlocking && !waitForThumbnailReady(origUri)) { 1020 Log.w(TAG, "original media doesn't exist or it's canceled."); 1021 return false; 1022 } else if (cancelRequest) { 1023 String groupId = uri.getQueryParameter("group_id"); 1024 boolean isVideo = "video".equals(uri.getPathSegments().get(1)); 1025 int pid = Binder.getCallingPid(); 1026 long id = -1; 1027 long gid = -1; 1028 1029 try { 1030 id = Long.parseLong(origId); 1031 gid = Long.parseLong(groupId); 1032 } catch (NumberFormatException ex) { 1033 // invalid cancel request 1034 return false; 1035 } 1036 1037 synchronized (mMediaThumbQueue) { 1038 if (mCurrentThumbRequest != null && 1039 matchThumbRequest(mCurrentThumbRequest, pid, id, gid, isVideo)) { 1040 synchronized (mCurrentThumbRequest) { 1041 mCurrentThumbRequest.mState = MediaThumbRequest.State.CANCEL; 1042 mCurrentThumbRequest.notifyAll(); 1043 } 1044 } 1045 for (MediaThumbRequest mtq : mMediaThumbQueue) { 1046 if (matchThumbRequest(mtq, pid, id, gid, isVideo)) { 1047 synchronized (mtq) { 1048 mtq.mState = MediaThumbRequest.State.CANCEL; 1049 mtq.notifyAll(); 1050 } 1051 1052 mMediaThumbQueue.remove(mtq); 1053 } 1054 } 1055 } 1056 } 1057 1058 if (origId != null) { 1059 qb.appendWhere(column + " = " + origId); 1060 } 1061 return true; 1062 } 1063 @SuppressWarnings("fallthrough") 1064 @Override 1065 public Cursor query(Uri uri, String[] projectionIn, String selection, 1066 String[] selectionArgs, String sort) { 1067 int table = URI_MATCHER.match(uri); 1068 1069 // Log.v(TAG, "query: uri="+uri+", selection="+selection); 1070 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1071 if (table == MEDIA_SCANNER) { 1072 if (mMediaScannerVolume == null) { 1073 return null; 1074 } else { 1075 // create a cursor to return volume currently being scanned by the media scanner 1076 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME}); 1077 c.addRow(new String[] {mMediaScannerVolume}); 1078 return c; 1079 } 1080 } 1081 1082 // Used temporarily (until we have unique media IDs) to get an identifier 1083 // for the current sd card, so that the music app doesn't have to use the 1084 // non-public getFatVolumeId method 1085 if (table == FS_ID) { 1086 MatrixCursor c = new MatrixCursor(new String[] {"fsid"}); 1087 c.addRow(new Integer[] {mVolumeId}); 1088 return c; 1089 } 1090 1091 String groupBy = null; 1092 DatabaseHelper database = getDatabaseForUri(uri); 1093 if (database == null) { 1094 return null; 1095 } 1096 SQLiteDatabase db = database.getReadableDatabase(); 1097 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1098 String limit = uri.getQueryParameter("limit"); 1099 boolean hasThumbnailId = false; 1100 1101 switch (table) { 1102 case IMAGES_MEDIA: 1103 qb.setTables("images"); 1104 if (uri.getQueryParameter("distinct") != null) 1105 qb.setDistinct(true); 1106 1107 // set the project map so that data dir is prepended to _data. 1108 //qb.setProjectionMap(mImagesProjectionMap, true); 1109 break; 1110 1111 case IMAGES_MEDIA_ID: 1112 qb.setTables("images"); 1113 if (uri.getQueryParameter("distinct") != null) 1114 qb.setDistinct(true); 1115 1116 // set the project map so that data dir is prepended to _data. 1117 //qb.setProjectionMap(mImagesProjectionMap, true); 1118 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 1119 break; 1120 1121 case IMAGES_THUMBNAILS_ID: 1122 hasThumbnailId = true; 1123 case IMAGES_THUMBNAILS: 1124 if (!queryThumbnail(qb, uri, "thumbnails", "image_id", hasThumbnailId)) { 1125 return null; 1126 } 1127 break; 1128 1129 case AUDIO_MEDIA: 1130 qb.setTables("audio "); 1131 break; 1132 1133 case AUDIO_MEDIA_ID: 1134 qb.setTables("audio"); 1135 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1136 break; 1137 1138 case AUDIO_MEDIA_ID_GENRES: 1139 qb.setTables("audio_genres"); 1140 qb.appendWhere("_id IN (SELECT genre_id FROM " + 1141 "audio_genres_map WHERE audio_id = " + 1142 uri.getPathSegments().get(3) + ")"); 1143 break; 1144 1145 case AUDIO_MEDIA_ID_GENRES_ID: 1146 qb.setTables("audio_genres"); 1147 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1148 break; 1149 1150 case AUDIO_MEDIA_ID_PLAYLISTS: 1151 qb.setTables("audio_playlists"); 1152 qb.appendWhere("_id IN (SELECT playlist_id FROM " + 1153 "audio_playlists_map WHERE audio_id = " + 1154 uri.getPathSegments().get(3) + ")"); 1155 break; 1156 1157 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1158 qb.setTables("audio_playlists"); 1159 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1160 break; 1161 1162 case AUDIO_GENRES: 1163 qb.setTables("audio_genres"); 1164 break; 1165 1166 case AUDIO_GENRES_ID: 1167 qb.setTables("audio_genres"); 1168 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1169 break; 1170 1171 case AUDIO_GENRES_ID_MEMBERS: 1172 qb.setTables("audio"); 1173 qb.appendWhere("_id IN (SELECT audio_id FROM " + 1174 "audio_genres_map WHERE genre_id = " + 1175 uri.getPathSegments().get(3) + ")"); 1176 break; 1177 1178 case AUDIO_GENRES_ID_MEMBERS_ID: 1179 qb.setTables("audio"); 1180 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1181 break; 1182 1183 case AUDIO_PLAYLISTS: 1184 qb.setTables("audio_playlists"); 1185 break; 1186 1187 case AUDIO_PLAYLISTS_ID: 1188 qb.setTables("audio_playlists"); 1189 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1190 break; 1191 1192 case AUDIO_PLAYLISTS_ID_MEMBERS: 1193 if (projectionIn != null) { 1194 for (int i = 0; i < projectionIn.length; i++) { 1195 if (projectionIn[i].equals("_id")) { 1196 projectionIn[i] = "audio_playlists_map._id AS _id"; 1197 } 1198 } 1199 } 1200 qb.setTables("audio_playlists_map, audio"); 1201 qb.appendWhere("audio._id = audio_id AND playlist_id = " 1202 + uri.getPathSegments().get(3)); 1203 break; 1204 1205 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1206 qb.setTables("audio"); 1207 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1208 break; 1209 1210 case VIDEO_MEDIA: 1211 qb.setTables("video"); 1212 break; 1213 1214 case VIDEO_MEDIA_ID: 1215 qb.setTables("video"); 1216 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1217 break; 1218 1219 case VIDEO_THUMBNAILS_ID: 1220 hasThumbnailId = true; 1221 case VIDEO_THUMBNAILS: 1222 if (!queryThumbnail(qb, uri, "videothumbnails", "video_id", hasThumbnailId)) { 1223 return null; 1224 } 1225 break; 1226 1227 case AUDIO_ARTISTS: 1228 qb.setTables("artist_info"); 1229 break; 1230 1231 case AUDIO_ARTISTS_ID: 1232 qb.setTables("artist_info"); 1233 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1234 break; 1235 1236 case AUDIO_ARTISTS_ID_ALBUMS: 1237 String aid = uri.getPathSegments().get(3); 1238 qb.setTables("audio LEFT OUTER JOIN album_art ON" + 1239 " audio.album_id=album_art.album_id"); 1240 qb.appendWhere("is_music=1 AND audio.album_id IN (SELECT album_id FROM " + 1241 "artists_albums_map WHERE artist_id = " + 1242 aid + ")"); 1243 groupBy = "audio.album_id"; 1244 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 1245 "count(CASE WHEN artist_id==" + aid + " THEN 'foo' ELSE NULL END) AS " + 1246 MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 1247 qb.setProjectionMap(sArtistAlbumsMap); 1248 break; 1249 1250 case AUDIO_ALBUMS: 1251 qb.setTables("album_info"); 1252 break; 1253 1254 case AUDIO_ALBUMS_ID: 1255 qb.setTables("album_info"); 1256 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1257 break; 1258 1259 case AUDIO_ALBUMART_ID: 1260 qb.setTables("album_art"); 1261 qb.appendWhere("album_id=" + uri.getPathSegments().get(3)); 1262 break; 1263 1264 case AUDIO_SEARCH_LEGACY: 1265 Log.w(TAG, "Legacy media search Uri used. Please update your code."); 1266 // fall through 1267 case AUDIO_SEARCH_FANCY: 1268 case AUDIO_SEARCH_BASIC: 1269 return doAudioSearch(db, qb, uri, projectionIn, selection, selectionArgs, sort, 1270 table, limit); 1271 1272 default: 1273 throw new IllegalStateException("Unknown URL: " + uri.toString()); 1274 } 1275 1276 // Log.v(TAG, "query = "+ qb.buildQuery(projectionIn, selection, selectionArgs, groupBy, null, sort, limit)); 1277 Cursor c = qb.query(db, projectionIn, selection, 1278 selectionArgs, groupBy, null, sort, limit); 1279 1280 if (c != null) { 1281 c.setNotificationUri(getContext().getContentResolver(), uri); 1282 } 1283 1284 return c; 1285 } 1286 1287 private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb, 1288 Uri uri, String[] projectionIn, String selection, 1289 String[] selectionArgs, String sort, int mode, 1290 String limit) { 1291 1292 String mSearchString = uri.toString().endsWith("/") ? "" : uri.getLastPathSegment(); 1293 mSearchString = mSearchString.replaceAll(" ", " ").trim().toLowerCase(); 1294 1295 String [] searchWords = mSearchString.length() > 0 ? 1296 mSearchString.split(" ") : new String[0]; 1297 String [] wildcardWords = new String[searchWords.length]; 1298 Collator col = Collator.getInstance(); 1299 col.setStrength(Collator.PRIMARY); 1300 int len = searchWords.length; 1301 for (int i = 0; i < len; i++) { 1302 // Because we match on individual words here, we need to remove words 1303 // like 'a' and 'the' that aren't part of the keys. 1304 wildcardWords[i] = 1305 (searchWords[i].equals("a") || searchWords[i].equals("an") || 1306 searchWords[i].equals("the")) ? "%" : 1307 '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%'; 1308 } 1309 1310 String where = ""; 1311 for (int i = 0; i < searchWords.length; i++) { 1312 if (i == 0) { 1313 where = "match LIKE ?"; 1314 } else { 1315 where += " AND match LIKE ?"; 1316 } 1317 } 1318 1319 qb.setTables("search"); 1320 String [] cols; 1321 if (mode == AUDIO_SEARCH_FANCY) { 1322 cols = mSearchColsFancy; 1323 } else if (mode == AUDIO_SEARCH_BASIC) { 1324 cols = mSearchColsBasic; 1325 } else { 1326 cols = mSearchColsLegacy; 1327 } 1328 return qb.query(db, cols, where, wildcardWords, null, null, null, limit); 1329 } 1330 1331 @Override 1332 public String getType(Uri url) 1333 { 1334 switch (URI_MATCHER.match(url)) { 1335 case IMAGES_MEDIA_ID: 1336 case AUDIO_MEDIA_ID: 1337 case AUDIO_GENRES_ID_MEMBERS_ID: 1338 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1339 case VIDEO_MEDIA_ID: 1340 Cursor c = query(url, MIME_TYPE_PROJECTION, null, null, null); 1341 if (c != null && c.getCount() == 1) { 1342 c.moveToFirst(); 1343 String mimeType = c.getString(1); 1344 c.deactivate(); 1345 return mimeType; 1346 } 1347 break; 1348 1349 case IMAGES_MEDIA: 1350 case IMAGES_THUMBNAILS: 1351 return Images.Media.CONTENT_TYPE; 1352 case IMAGES_THUMBNAILS_ID: 1353 return "image/jpeg"; 1354 1355 case AUDIO_MEDIA: 1356 case AUDIO_GENRES_ID_MEMBERS: 1357 case AUDIO_PLAYLISTS_ID_MEMBERS: 1358 return Audio.Media.CONTENT_TYPE; 1359 1360 case AUDIO_GENRES: 1361 case AUDIO_MEDIA_ID_GENRES: 1362 return Audio.Genres.CONTENT_TYPE; 1363 case AUDIO_GENRES_ID: 1364 case AUDIO_MEDIA_ID_GENRES_ID: 1365 return Audio.Genres.ENTRY_CONTENT_TYPE; 1366 case AUDIO_PLAYLISTS: 1367 case AUDIO_MEDIA_ID_PLAYLISTS: 1368 return Audio.Playlists.CONTENT_TYPE; 1369 case AUDIO_PLAYLISTS_ID: 1370 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1371 return Audio.Playlists.ENTRY_CONTENT_TYPE; 1372 1373 case VIDEO_MEDIA: 1374 return Video.Media.CONTENT_TYPE; 1375 } 1376 throw new IllegalStateException("Unknown URL"); 1377 } 1378 1379 /** 1380 * Ensures there is a file in the _data column of values, if one isn't 1381 * present a new file is created. 1382 * 1383 * @param initialValues the values passed to insert by the caller 1384 * @return the new values 1385 */ 1386 private ContentValues ensureFile(boolean internal, ContentValues initialValues, 1387 String preferredExtension, String directoryName) { 1388 ContentValues values; 1389 String file = initialValues.getAsString("_data"); 1390 if (TextUtils.isEmpty(file)) { 1391 file = generateFileName(internal, preferredExtension, directoryName); 1392 values = new ContentValues(initialValues); 1393 values.put("_data", file); 1394 } else { 1395 values = initialValues; 1396 } 1397 1398 if (!ensureFileExists(file)) { 1399 throw new IllegalStateException("Unable to create new file: " + file); 1400 } 1401 return values; 1402 } 1403 1404 @Override 1405 public int bulkInsert(Uri uri, ContentValues values[]) { 1406 int match = URI_MATCHER.match(uri); 1407 if (match == VOLUMES) { 1408 return super.bulkInsert(uri, values); 1409 } 1410 DatabaseHelper database = getDatabaseForUri(uri); 1411 if (database == null) { 1412 throw new UnsupportedOperationException( 1413 "Unknown URI: " + uri); 1414 } 1415 SQLiteDatabase db = database.getWritableDatabase(); 1416 1417 if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) { 1418 return playlistBulkInsert(db, uri, values); 1419 } 1420 1421 db.beginTransaction(); 1422 int numInserted = 0; 1423 try { 1424 int len = values.length; 1425 for (int i = 0; i < len; i++) { 1426 insertInternal(uri, values[i]); 1427 } 1428 numInserted = len; 1429 db.setTransactionSuccessful(); 1430 } finally { 1431 db.endTransaction(); 1432 } 1433 getContext().getContentResolver().notifyChange(uri, null); 1434 return numInserted; 1435 } 1436 1437 @Override 1438 public Uri insert(Uri uri, ContentValues initialValues) 1439 { 1440 Uri newUri = insertInternal(uri, initialValues); 1441 if (newUri != null) { 1442 getContext().getContentResolver().notifyChange(uri, null); 1443 } 1444 return newUri; 1445 } 1446 1447 private int playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[]) { 1448 DatabaseUtils.InsertHelper helper = 1449 new DatabaseUtils.InsertHelper(db, "audio_playlists_map"); 1450 int audioidcolidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID); 1451 int playlistididx = helper.getColumnIndex(Audio.Playlists.Members.PLAYLIST_ID); 1452 int playorderidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 1453 long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 1454 1455 db.beginTransaction(); 1456 int numInserted = 0; 1457 try { 1458 int len = values.length; 1459 for (int i = 0; i < len; i++) { 1460 helper.prepareForInsert(); 1461 // getting the raw Object and converting it long ourselves saves 1462 // an allocation (the alternative is ContentValues.getAsLong, which 1463 // returns a Long object) 1464 long audioid = ((Number) values[i].get( 1465 MediaStore.Audio.Playlists.Members.AUDIO_ID)).longValue(); 1466 helper.bind(audioidcolidx, audioid); 1467 helper.bind(playlistididx, playlistId); 1468 // convert to int ourselves to save an allocation. 1469 int playorder = ((Number) values[i].get( 1470 MediaStore.Audio.Playlists.Members.PLAY_ORDER)).intValue(); 1471 helper.bind(playorderidx, playorder); 1472 helper.execute(); 1473 } 1474 numInserted = len; 1475 db.setTransactionSuccessful(); 1476 } finally { 1477 db.endTransaction(); 1478 helper.close(); 1479 } 1480 getContext().getContentResolver().notifyChange(uri, null); 1481 return numInserted; 1482 } 1483 1484 private Uri insertInternal(Uri uri, ContentValues initialValues) { 1485 long rowId; 1486 int match = URI_MATCHER.match(uri); 1487 1488 // Log.v(TAG, "insertInternal: "+uri+", initValues="+initialValues); 1489 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1490 if (match == MEDIA_SCANNER) { 1491 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 1492 return MediaStore.getMediaScannerUri(); 1493 } 1494 1495 Uri newUri = null; 1496 DatabaseHelper database = getDatabaseForUri(uri); 1497 if (database == null && match != VOLUMES) { 1498 throw new UnsupportedOperationException( 1499 "Unknown URI: " + uri); 1500 } 1501 SQLiteDatabase db = (match == VOLUMES ? null : database.getWritableDatabase()); 1502 1503 if (initialValues == null) { 1504 initialValues = new ContentValues(); 1505 } 1506 1507 switch (match) { 1508 case IMAGES_MEDIA: { 1509 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/Camera"); 1510 1511 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1512 String data = values.getAsString(MediaColumns.DATA); 1513 if (! values.containsKey(MediaColumns.DISPLAY_NAME)) { 1514 computeDisplayName(data, values); 1515 } 1516 computeBucketValues(data, values); 1517 rowId = db.insert("images", "name", values); 1518 1519 if (rowId > 0) { 1520 newUri = ContentUris.withAppendedId( 1521 Images.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 1522 requestMediaThumbnail(data, newUri, MediaThumbRequest.PRIORITY_NORMAL, 0); 1523 } 1524 break; 1525 } 1526 1527 // This will be triggered by requestMediaThumbnail (see getThumbnailUri) 1528 case IMAGES_THUMBNAILS: { 1529 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", 1530 "DCIM/.thumbnails"); 1531 rowId = db.insert("thumbnails", "name", values); 1532 if (rowId > 0) { 1533 newUri = ContentUris.withAppendedId(Images.Thumbnails. 1534 getContentUri(uri.getPathSegments().get(0)), rowId); 1535 } 1536 break; 1537 } 1538 1539 // This is currently only used by MICRO_KIND video thumbnail (see getThumbnailUri) 1540 case VIDEO_THUMBNAILS: { 1541 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", 1542 "DCIM/.thumbnails"); 1543 rowId = db.insert("videothumbnails", "name", values); 1544 if (rowId > 0) { 1545 newUri = ContentUris.withAppendedId(Video.Thumbnails. 1546 getContentUri(uri.getPathSegments().get(0)), rowId); 1547 } 1548 break; 1549 } 1550 1551 case AUDIO_MEDIA: { 1552 // SQLite Views are read-only, so we need to deconstruct this 1553 // insert and do inserts into the underlying tables. 1554 // If doing this here turns out to be a performance bottleneck, 1555 // consider moving this to native code and using triggers on 1556 // the view. 1557 ContentValues values = new ContentValues(initialValues); 1558 1559 // Insert the artist into the artist table and remove it from 1560 // the input values 1561 Object so = values.get("artist"); 1562 String s = (so == null ? "" : so.toString()); 1563 values.remove("artist"); 1564 long artistRowId; 1565 HashMap<String, Long> artistCache = database.mArtistCache; 1566 String path = values.getAsString("_data"); 1567 synchronized(artistCache) { 1568 Long temp = artistCache.get(s); 1569 if (temp == null) { 1570 artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist", 1571 s, s, path, 0, null, artistCache, uri); 1572 } else { 1573 artistRowId = temp.longValue(); 1574 } 1575 } 1576 String artist = s; 1577 1578 // Do the same for the album field 1579 so = values.get("album"); 1580 s = (so == null ? "" : so.toString()); 1581 values.remove("album"); 1582 long albumRowId; 1583 HashMap<String, Long> albumCache = database.mAlbumCache; 1584 synchronized(albumCache) { 1585 int albumhash = path.substring(0, path.lastIndexOf('/')).hashCode(); 1586 String cacheName = s + albumhash; 1587 Long temp = albumCache.get(cacheName); 1588 if (temp == null) { 1589 albumRowId = getKeyIdForName(db, "albums", "album_key", "album", 1590 s, cacheName, path, albumhash, artist, albumCache, uri); 1591 } else { 1592 albumRowId = temp; 1593 } 1594 } 1595 1596 values.put("artist_id", Integer.toString((int)artistRowId)); 1597 values.put("album_id", Integer.toString((int)albumRowId)); 1598 so = values.getAsString("title"); 1599 s = (so == null ? "" : so.toString()); 1600 values.put("title_key", MediaStore.Audio.keyFor(s)); 1601 // do a final trim of the title, in case it started with the special 1602 // "sort first" character (ascii \001) 1603 values.remove("title"); 1604 values.put("title", s.trim()); 1605 1606 computeDisplayName(values.getAsString("_data"), values); 1607 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1608 1609 rowId = db.insert("audio_meta", "duration", values); 1610 if (rowId > 0) { 1611 newUri = ContentUris.withAppendedId(Audio.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 1612 } 1613 break; 1614 } 1615 1616 case AUDIO_MEDIA_ID_GENRES: { 1617 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 1618 ContentValues values = new ContentValues(initialValues); 1619 values.put(Audio.Genres.Members.AUDIO_ID, audioId); 1620 rowId = db.insert("audio_genres_map", "genre_id", values); 1621 if (rowId > 0) { 1622 newUri = ContentUris.withAppendedId(uri, rowId); 1623 } 1624 break; 1625 } 1626 1627 case AUDIO_MEDIA_ID_PLAYLISTS: { 1628 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 1629 ContentValues values = new ContentValues(initialValues); 1630 values.put(Audio.Playlists.Members.AUDIO_ID, audioId); 1631 rowId = db.insert("audio_playlists_map", "playlist_id", 1632 values); 1633 if (rowId > 0) { 1634 newUri = ContentUris.withAppendedId(uri, rowId); 1635 } 1636 break; 1637 } 1638 1639 case AUDIO_GENRES: { 1640 rowId = db.insert("audio_genres", "audio_id", initialValues); 1641 if (rowId > 0) { 1642 newUri = ContentUris.withAppendedId(Audio.Genres.getContentUri(uri.getPathSegments().get(0)), rowId); 1643 } 1644 break; 1645 } 1646 1647 case AUDIO_GENRES_ID_MEMBERS: { 1648 Long genreId = Long.parseLong(uri.getPathSegments().get(3)); 1649 ContentValues values = new ContentValues(initialValues); 1650 values.put(Audio.Genres.Members.GENRE_ID, genreId); 1651 rowId = db.insert("audio_genres_map", "genre_id", values); 1652 if (rowId > 0) { 1653 newUri = ContentUris.withAppendedId(uri, rowId); 1654 } 1655 break; 1656 } 1657 1658 case AUDIO_PLAYLISTS: { 1659 ContentValues values = new ContentValues(initialValues); 1660 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 1661 rowId = db.insert("audio_playlists", "name", initialValues); 1662 if (rowId > 0) { 1663 newUri = ContentUris.withAppendedId(Audio.Playlists.getContentUri(uri.getPathSegments().get(0)), rowId); 1664 } 1665 break; 1666 } 1667 1668 case AUDIO_PLAYLISTS_ID: 1669 case AUDIO_PLAYLISTS_ID_MEMBERS: { 1670 Long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 1671 ContentValues values = new ContentValues(initialValues); 1672 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId); 1673 rowId = db.insert("audio_playlists_map", "playlist_id", values); 1674 if (rowId > 0) { 1675 newUri = ContentUris.withAppendedId(uri, rowId); 1676 } 1677 break; 1678 } 1679 1680 case VIDEO_MEDIA: { 1681 ContentValues values = ensureFile(database.mInternal, initialValues, ".3gp", "video"); 1682 String data = values.getAsString("_data"); 1683 computeDisplayName(data, values); 1684 computeBucketValues(data, values); 1685 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1686 rowId = db.insert("video", "artist", values); 1687 if (rowId > 0) { 1688 newUri = ContentUris.withAppendedId(Video.Media.getContentUri( 1689 uri.getPathSegments().get(0)), rowId); 1690 requestMediaThumbnail(data, newUri, MediaThumbRequest.PRIORITY_NORMAL, 0); 1691 } 1692 break; 1693 } 1694 1695 case AUDIO_ALBUMART: 1696 if (database.mInternal) { 1697 throw new UnsupportedOperationException("no internal album art allowed"); 1698 } 1699 ContentValues values = null; 1700 try { 1701 values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 1702 } catch (IllegalStateException ex) { 1703 // probably no more room to store albumthumbs 1704 values = initialValues; 1705 } 1706 rowId = db.insert("album_art", "_data", values); 1707 if (rowId > 0) { 1708 newUri = ContentUris.withAppendedId(uri, rowId); 1709 } 1710 break; 1711 1712 case VOLUMES: 1713 return attachVolume(initialValues.getAsString("name")); 1714 1715 default: 1716 throw new UnsupportedOperationException("Invalid URI " + uri); 1717 } 1718 1719 return newUri; 1720 } 1721 1722 @Override 1723 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1724 throws OperationApplicationException { 1725 1726 // The operations array provides no overall information about the URI(s) being operated 1727 // on, so begin a transaction for ALL of the databases. 1728 DatabaseHelper ihelper = getDatabaseForUri(MediaStore.Audio.Media.INTERNAL_CONTENT_URI); 1729 DatabaseHelper ehelper = getDatabaseForUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI); 1730 SQLiteDatabase idb = ihelper.getWritableDatabase(); 1731 idb.beginTransaction(); 1732 SQLiteDatabase edb = null; 1733 if (ehelper != null) { 1734 edb = ehelper.getWritableDatabase(); 1735 edb.beginTransaction(); 1736 } 1737 try { 1738 ContentProviderResult[] result = super.applyBatch(operations); 1739 idb.setTransactionSuccessful(); 1740 if (edb != null) { 1741 edb.setTransactionSuccessful(); 1742 } 1743 // Rather than sending targeted change notifications for every Uri 1744 // affected by the batch operation, just invalidate the entire internal 1745 // and external name space. 1746 ContentResolver res = getContext().getContentResolver(); 1747 res.notifyChange(Uri.parse("content://media/"), null); 1748 return result; 1749 } finally { 1750 idb.endTransaction(); 1751 if (edb != null) { 1752 edb.endTransaction(); 1753 } 1754 } 1755 } 1756 1757 1758 private MediaThumbRequest requestMediaThumbnail(String path, Uri uri, int priority, long magic) { 1759 synchronized (mMediaThumbQueue) { 1760 MediaThumbRequest req = null; 1761 try { 1762 req = new MediaThumbRequest( 1763 getContext().getContentResolver(), path, uri, priority, magic); 1764 mMediaThumbQueue.add(req); 1765 // Trigger the handler. 1766 Message msg = mThumbHandler.obtainMessage(IMAGE_THUMB); 1767 msg.sendToTarget(); 1768 } catch (Throwable t) { 1769 Log.w(TAG, t); 1770 } 1771 return req; 1772 } 1773 } 1774 1775 private String generateFileName(boolean internal, String preferredExtension, String directoryName) 1776 { 1777 // create a random file 1778 String name = String.valueOf(System.currentTimeMillis()); 1779 1780 if (internal) { 1781 throw new UnsupportedOperationException("Writing to internal storage is not supported."); 1782// return Environment.getDataDirectory() 1783// + "/" + directoryName + "/" + name + preferredExtension; 1784 } else { 1785 return Environment.getExternalStorageDirectory() 1786 + "/" + directoryName + "/" + name + preferredExtension; 1787 } 1788 } 1789 1790 private boolean ensureFileExists(String path) { 1791 File file = new File(path); 1792 if (file.exists()) { 1793 return true; 1794 } else { 1795 // we will not attempt to create the first directory in the path 1796 // (for example, do not create /sdcard if the SD card is not mounted) 1797 int secondSlash = path.indexOf('/', 1); 1798 if (secondSlash < 1) return false; 1799 String directoryPath = path.substring(0, secondSlash); 1800 File directory = new File(directoryPath); 1801 if (!directory.exists()) 1802 return false; 1803 file.getParentFile().mkdirs(); 1804 try { 1805 return file.createNewFile(); 1806 } catch(IOException ioe) { 1807 Log.e(TAG, "File creation failed", ioe); 1808 } 1809 return false; 1810 } 1811 } 1812 1813 private static final class GetTableAndWhereOutParameter { 1814 public String table; 1815 public String where; 1816 } 1817 1818 static final GetTableAndWhereOutParameter sGetTableAndWhereParam = 1819 new GetTableAndWhereOutParameter(); 1820 1821 private void getTableAndWhere(Uri uri, int match, String userWhere, 1822 GetTableAndWhereOutParameter out) { 1823 String where = null; 1824 switch (match) { 1825 case IMAGES_MEDIA: 1826 out.table = "images"; 1827 break; 1828 1829 case IMAGES_MEDIA_ID: 1830 out.table = "images"; 1831 where = "_id = " + uri.getPathSegments().get(3); 1832 break; 1833 1834 case IMAGES_THUMBNAILS_ID: 1835 where = "_id=" + uri.getPathSegments().get(3); 1836 case IMAGES_THUMBNAILS: 1837 out.table = "thumbnails"; 1838 break; 1839 1840 case AUDIO_MEDIA: 1841 out.table = "audio"; 1842 break; 1843 1844 case AUDIO_MEDIA_ID: 1845 out.table = "audio"; 1846 where = "_id=" + uri.getPathSegments().get(3); 1847 break; 1848 1849 case AUDIO_MEDIA_ID_GENRES: 1850 out.table = "audio_genres"; 1851 where = "audio_id=" + uri.getPathSegments().get(3); 1852 break; 1853 1854 case AUDIO_MEDIA_ID_GENRES_ID: 1855 out.table = "audio_genres"; 1856 where = "audio_id=" + uri.getPathSegments().get(3) + 1857 " AND genre_id=" + uri.getPathSegments().get(5); 1858 break; 1859 1860 case AUDIO_MEDIA_ID_PLAYLISTS: 1861 out.table = "audio_playlists"; 1862 where = "audio_id=" + uri.getPathSegments().get(3); 1863 break; 1864 1865 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1866 out.table = "audio_playlists"; 1867 where = "audio_id=" + uri.getPathSegments().get(3) + 1868 " AND playlists_id=" + uri.getPathSegments().get(5); 1869 break; 1870 1871 case AUDIO_GENRES: 1872 out.table = "audio_genres"; 1873 break; 1874 1875 case AUDIO_GENRES_ID: 1876 out.table = "audio_genres"; 1877 where = "_id=" + uri.getPathSegments().get(3); 1878 break; 1879 1880 case AUDIO_GENRES_ID_MEMBERS: 1881 out.table = "audio_genres"; 1882 where = "genre_id=" + uri.getPathSegments().get(3); 1883 break; 1884 1885 case AUDIO_GENRES_ID_MEMBERS_ID: 1886 out.table = "audio_genres"; 1887 where = "genre_id=" + uri.getPathSegments().get(3) + 1888 " AND audio_id =" + uri.getPathSegments().get(5); 1889 break; 1890 1891 case AUDIO_PLAYLISTS: 1892 out.table = "audio_playlists"; 1893 break; 1894 1895 case AUDIO_PLAYLISTS_ID: 1896 out.table = "audio_playlists"; 1897 where = "_id=" + uri.getPathSegments().get(3); 1898 break; 1899 1900 case AUDIO_PLAYLISTS_ID_MEMBERS: 1901 out.table = "audio_playlists_map"; 1902 where = "playlist_id=" + uri.getPathSegments().get(3); 1903 break; 1904 1905 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1906 out.table = "audio_playlists_map"; 1907 where = "playlist_id=" + uri.getPathSegments().get(3) + 1908 " AND _id=" + uri.getPathSegments().get(5); 1909 break; 1910 1911 case AUDIO_ALBUMART_ID: 1912 out.table = "album_art"; 1913 where = "album_id=" + uri.getPathSegments().get(3); 1914 break; 1915 1916 case VIDEO_MEDIA: 1917 out.table = "video"; 1918 break; 1919 1920 case VIDEO_MEDIA_ID: 1921 out.table = "video"; 1922 where = "_id=" + uri.getPathSegments().get(3); 1923 break; 1924 1925 case VIDEO_THUMBNAILS_ID: 1926 where = "_id=" + uri.getPathSegments().get(3); 1927 case VIDEO_THUMBNAILS: 1928 out.table = "videothumbnails"; 1929 break; 1930 1931 default: 1932 throw new UnsupportedOperationException( 1933 "Unknown or unsupported URL: " + uri.toString()); 1934 } 1935 1936 // Add in the user requested WHERE clause, if needed 1937 if (!TextUtils.isEmpty(userWhere)) { 1938 if (!TextUtils.isEmpty(where)) { 1939 out.where = where + " AND (" + userWhere + ")"; 1940 } else { 1941 out.where = userWhere; 1942 } 1943 } else { 1944 out.where = where; 1945 } 1946 } 1947 1948 @Override 1949 public int delete(Uri uri, String userWhere, String[] whereArgs) { 1950 int count; 1951 int match = URI_MATCHER.match(uri); 1952 1953 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1954 if (match == MEDIA_SCANNER) { 1955 if (mMediaScannerVolume == null) { 1956 return 0; 1957 } 1958 mMediaScannerVolume = null; 1959 return 1; 1960 } 1961 1962 if (match != VOLUMES_ID) { 1963 DatabaseHelper database = getDatabaseForUri(uri); 1964 if (database == null) { 1965 throw new UnsupportedOperationException( 1966 "Unknown URI: " + uri); 1967 } 1968 SQLiteDatabase db = database.getWritableDatabase(); 1969 1970 synchronized (sGetTableAndWhereParam) { 1971 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 1972 switch (match) { 1973 case AUDIO_MEDIA: 1974 case AUDIO_MEDIA_ID: 1975 count = db.delete("audio_meta", 1976 sGetTableAndWhereParam.where, whereArgs); 1977 break; 1978 default: 1979 count = db.delete(sGetTableAndWhereParam.table, 1980 sGetTableAndWhereParam.where, whereArgs); 1981 break; 1982 } 1983 getContext().getContentResolver().notifyChange(uri, null); 1984 } 1985 } else { 1986 detachVolume(uri); 1987 count = 1; 1988 } 1989 1990 return count; 1991 } 1992 1993 @Override 1994 public int update(Uri uri, ContentValues initialValues, String userWhere, 1995 String[] whereArgs) { 1996 int count; 1997 // Log.v(TAG, "update for uri="+uri+", initValues="+initialValues); 1998 int match = URI_MATCHER.match(uri); 1999 DatabaseHelper database = getDatabaseForUri(uri); 2000 if (database == null) { 2001 throw new UnsupportedOperationException( 2002 "Unknown URI: " + uri); 2003 } 2004 SQLiteDatabase db = database.getWritableDatabase(); 2005 2006 synchronized (sGetTableAndWhereParam) { 2007 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 2008 2009 switch (match) { 2010 case AUDIO_MEDIA: 2011 case AUDIO_MEDIA_ID: 2012 { 2013 ContentValues values = new ContentValues(initialValues); 2014 // Insert the artist into the artist table and remove it from 2015 // the input values 2016 String artist = values.getAsString("artist"); 2017 if (artist != null) { 2018 values.remove("artist"); 2019 long artistRowId; 2020 HashMap<String, Long> artistCache = database.mArtistCache; 2021 synchronized(artistCache) { 2022 Long temp = artistCache.get(artist); 2023 if (temp == null) { 2024 artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist", 2025 artist, artist, null, 0, null, artistCache, uri); 2026 } else { 2027 artistRowId = temp.longValue(); 2028 } 2029 } 2030 values.put("artist_id", Integer.toString((int)artistRowId)); 2031 } 2032 2033 // Do the same for the album field. 2034 String so = values.getAsString("album"); 2035 if (so != null) { 2036 String path = values.getAsString("_data"); 2037 int albumHash = 0; 2038 if (path == null) { 2039 // If the path is null, we don't have a hash for the file in question. 2040 Log.w(TAG, "Update without specified path."); 2041 } else { 2042 albumHash = path.substring(0, path.lastIndexOf('/')).hashCode(); 2043 } 2044 String s = so.toString(); 2045 values.remove("album"); 2046 long albumRowId; 2047 HashMap<String, Long> albumCache = database.mAlbumCache; 2048 synchronized(albumCache) { 2049 String cacheName = s + albumHash; 2050 Long temp = albumCache.get(cacheName); 2051 if (temp == null) { 2052 albumRowId = getKeyIdForName(db, "albums", "album_key", "album", 2053 s, cacheName, path, albumHash, artist, albumCache, uri); 2054 } else { 2055 albumRowId = temp.longValue(); 2056 } 2057 } 2058 values.put("album_id", Integer.toString((int)albumRowId)); 2059 } 2060 2061 // don't allow the title_key field to be updated directly 2062 values.remove("title_key"); 2063 // If the title field is modified, update the title_key 2064 so = values.getAsString("title"); 2065 if (so != null) { 2066 String s = so.toString(); 2067 values.put("title_key", MediaStore.Audio.keyFor(s)); 2068 // do a final trim of the title, in case it started with the special 2069 // "sort first" character (ascii \001) 2070 values.remove("title"); 2071 values.put("title", s.trim()); 2072 } 2073 2074 count = db.update("audio_meta", values, sGetTableAndWhereParam.where, 2075 whereArgs); 2076 } 2077 break; 2078 case IMAGES_MEDIA: 2079 case IMAGES_MEDIA_ID: 2080 case VIDEO_MEDIA: 2081 case VIDEO_MEDIA_ID: 2082 { 2083 ContentValues values = new ContentValues(initialValues); 2084 // Don't allow bucket id or display name to be updated directly. 2085 // The same names are used for both images and table columns, so 2086 // we use the ImageColumns constants here. 2087 values.remove(ImageColumns.BUCKET_ID); 2088 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 2089 // If the data is being modified update the bucket values 2090 String data = values.getAsString(MediaColumns.DATA); 2091 if (data != null) { 2092 computeBucketValues(data, values); 2093 } 2094 count = db.update(sGetTableAndWhereParam.table, values, 2095 sGetTableAndWhereParam.where, whereArgs); 2096 // if this is a request from MediaScanner, DATA should contains file path 2097 // we only process update request from media scanner, otherwise the requests 2098 // could be duplicate. 2099 if (count > 0 && values.getAsString(MediaStore.MediaColumns.DATA) != null) { 2100 Cursor c = db.query(sGetTableAndWhereParam.table, 2101 READY_FLAG_PROJECTION, sGetTableAndWhereParam.where, 2102 whereArgs, null, null, null); 2103 if (c != null) { 2104 while (c.moveToNext()) { 2105 long magic = c.getLong(2); 2106 if (magic == 0) { 2107 requestMediaThumbnail(c.getString(1), uri, 2108 MediaThumbRequest.PRIORITY_NORMAL, 0); 2109 } 2110 } 2111 c.close(); 2112 } 2113 } 2114 } 2115 break; 2116 2117 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 2118 String moveit = uri.getQueryParameter("move"); 2119 if (moveit != null) { 2120 String key = MediaStore.Audio.Playlists.Members.PLAY_ORDER; 2121 if (initialValues.containsKey(key)) { 2122 int newpos = initialValues.getAsInteger(key); 2123 List <String> segments = uri.getPathSegments(); 2124 long playlist = Long.valueOf(segments.get(3)); 2125 int oldpos = Integer.valueOf(segments.get(5)); 2126 return movePlaylistEntry(db, playlist, oldpos, newpos); 2127 } 2128 throw new IllegalArgumentException("Need to specify " + key + 2129 " when using 'move' parameter"); 2130 } 2131 // fall through 2132 default: 2133 count = db.update(sGetTableAndWhereParam.table, initialValues, 2134 sGetTableAndWhereParam.where, whereArgs); 2135 break; 2136 } 2137 } 2138 // in a transaction, the code that began the transaction should be taking 2139 // care of notifications once it ends the transaction successfully 2140 if (count > 0 && !db.inTransaction()) { 2141 getContext().getContentResolver().notifyChange(uri, null); 2142 } 2143 return count; 2144 } 2145 2146 private int movePlaylistEntry(SQLiteDatabase db, long playlist, int from, int to) { 2147 if (from == to) { 2148 return 0; 2149 } 2150 db.beginTransaction(); 2151 try { 2152 int numlines = 0; 2153 db.execSQL("UPDATE audio_playlists_map SET play_order=-1" + 2154 " WHERE play_order=" + from + 2155 " AND playlist_id=" + playlist); 2156 // We could just run both of the next two statements, but only one of 2157 // of them will actually do anything, so might as well skip the compile 2158 // and execute steps. 2159 if (from < to) { 2160 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order-1" + 2161 " WHERE play_order<=" + to + " AND play_order>" + from + 2162 " AND playlist_id=" + playlist); 2163 numlines = to - from + 1; 2164 } else { 2165 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order+1" + 2166 " WHERE play_order>=" + to + " AND play_order<" + from + 2167 " AND playlist_id=" + playlist); 2168 numlines = from - to + 1; 2169 } 2170 db.execSQL("UPDATE audio_playlists_map SET play_order=" + to + 2171 " WHERE play_order=-1 AND playlist_id=" + playlist); 2172 db.setTransactionSuccessful(); 2173 Uri uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI 2174 .buildUpon().appendEncodedPath(String.valueOf(playlist)).build(); 2175 getContext().getContentResolver().notifyChange(uri, null); 2176 return numlines; 2177 } finally { 2178 db.endTransaction(); 2179 } 2180 } 2181 2182 private static final String[] openFileColumns = new String[] { 2183 MediaStore.MediaColumns.DATA, 2184 }; 2185 2186 @Override 2187 public ParcelFileDescriptor openFile(Uri uri, String mode) 2188 throws FileNotFoundException { 2189 2190 ParcelFileDescriptor pfd = null; 2191 2192 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_FILE_ID) { 2193 // get album art for the specified media file 2194 DatabaseHelper database = getDatabaseForUri(uri); 2195 if (database == null) { 2196 throw new IllegalStateException("Couldn't open database for " + uri); 2197 } 2198 SQLiteDatabase db = database.getReadableDatabase(); 2199 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2200 int songid = Integer.parseInt(uri.getPathSegments().get(3)); 2201 qb.setTables("audio_meta"); 2202 qb.appendWhere("_id=" + songid); 2203 Cursor c = qb.query(db, 2204 new String [] { 2205 MediaStore.Audio.Media.DATA, 2206 MediaStore.Audio.Media.ALBUM_ID }, 2207 null, null, null, null, null); 2208 if (c.moveToFirst()) { 2209 String audiopath = c.getString(0); 2210 int albumid = c.getInt(1); 2211 // Try to get existing album art for this album first, which 2212 // could possibly have been obtained from a different file. 2213 // If that fails, try to get it from this specific file. 2214 Uri newUri = ContentUris.withAppendedId(ALBUMART_URI, albumid); 2215 try { 2216 pfd = openFile(newUri, mode); // recursive call 2217 } catch (FileNotFoundException ex) { 2218 // That didn't work, now try to get it from the specific file 2219 pfd = getThumb(db, audiopath, albumid, null); 2220 } 2221 } 2222 c.close(); 2223 return pfd; 2224 } 2225 2226 try { 2227 pfd = openFileHelper(uri, mode); 2228 } catch (FileNotFoundException ex) { 2229 if (mode.contains("w")) { 2230 // if the file couldn't be created, we shouldn't extract album art 2231 throw ex; 2232 } 2233 2234 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_ID) { 2235 // Tried to open an album art file which does not exist. Regenerate. 2236 DatabaseHelper database = getDatabaseForUri(uri); 2237 if (database == null) { 2238 throw ex; 2239 } 2240 SQLiteDatabase db = database.getReadableDatabase(); 2241 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2242 int albumid = Integer.parseInt(uri.getPathSegments().get(3)); 2243 qb.setTables("audio_meta"); 2244 qb.appendWhere("album_id=" + albumid); 2245 Cursor c = qb.query(db, 2246 new String [] { 2247 MediaStore.Audio.Media.DATA }, 2248 null, null, null, null, null); 2249 if (c.moveToFirst()) { 2250 String audiopath = c.getString(0); 2251 pfd = getThumb(db, audiopath, albumid, uri); 2252 } 2253 c.close(); 2254 } else { 2255 // Opening the file failed. See if we can interpret the path as 2256 // a Uri and open that instead. This lets items in the media 2257 // database transparently refer to items in other content providers. 2258 // TODO figure out some way to detect recursion and abort 2259 // (using ThreadLocal might be appropriate for that). 2260 ContentResolver res = getContext().getContentResolver(); 2261 Cursor c = res.query(uri, new String[] {"_data"}, null, null, null); 2262 if (c != null) { 2263 try { 2264 if (c.moveToFirst()) { 2265 String path = c.getString(0); 2266 Uri newUri = Uri.parse(path); 2267 return res.openFileDescriptor(newUri, mode); 2268 } 2269 } finally { 2270 c.close(); 2271 } 2272 } 2273 } 2274 if (pfd == null) { 2275 throw ex; 2276 } 2277 } 2278 return pfd; 2279 } 2280 2281 private class Worker implements Runnable { 2282 private final Object mLock = new Object(); 2283 private Looper mLooper; 2284 2285 Worker(String name) { 2286 Thread t = new Thread(null, this, name); 2287 t.start(); 2288 synchronized (mLock) { 2289 while (mLooper == null) { 2290 try { 2291 mLock.wait(); 2292 } catch (InterruptedException ex) { 2293 } 2294 } 2295 } 2296 } 2297 2298 public Looper getLooper() { 2299 return mLooper; 2300 } 2301 2302 public void run() { 2303 synchronized (mLock) { 2304 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 2305 Looper.prepare(); 2306 mLooper = Looper.myLooper(); 2307 mLock.notifyAll(); 2308 } 2309 Looper.loop(); 2310 } 2311 2312 public void quit() { 2313 mLooper.quit(); 2314 } 2315 } 2316 2317 private class ThumbData { 2318 SQLiteDatabase db; 2319 String path; 2320 long album_id; 2321 Uri albumart_uri; 2322 } 2323 2324 private void makeThumbAsync(SQLiteDatabase db, String path, long album_id) { 2325 synchronized (mPendingThumbs) { 2326 if (mPendingThumbs.contains(path)) { 2327 // There's already a request to make an album art thumbnail 2328 // for this audio file in the queue. 2329 return; 2330 } 2331 2332 mPendingThumbs.add(path); 2333 } 2334 2335 ThumbData d = new ThumbData(); 2336 d.db = db; 2337 d.path = path; 2338 d.album_id = album_id; 2339 d.albumart_uri = ContentUris.withAppendedId(mAlbumArtBaseUri, album_id); 2340 2341 // Instead of processing thumbnail requests in the order they were 2342 // received we instead process them stack-based, i.e. LIFO. 2343 // The idea behind this is that the most recently requested thumbnails 2344 // are most likely the ones still in the user's view, whereas those 2345 // requested earlier may have already scrolled off. 2346 synchronized (mThumbRequestStack) { 2347 mThumbRequestStack.push(d); 2348 } 2349 2350 // Trigger the handler. 2351 Message msg = mThumbHandler.obtainMessage(ALBUM_THUMB); 2352 msg.sendToTarget(); 2353 } 2354 2355 // Extract compressed image data from the audio file itself or, if that fails, 2356 // look for a file "AlbumArt.jpg" in the containing directory. 2357 private static byte[] getCompressedAlbumArt(Context context, String path) { 2358 byte[] compressed = null; 2359 2360 try { 2361 File f = new File(path); 2362 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, 2363 ParcelFileDescriptor.MODE_READ_ONLY); 2364 2365 MediaScanner scanner = new MediaScanner(context); 2366 compressed = scanner.extractAlbumArt(pfd.getFileDescriptor()); 2367 pfd.close(); 2368 2369 // If no embedded art exists, look for a suitable image file in the 2370 // same directory as the media file, except if that directory is 2371 // is the root directory of the sd card or the download directory. 2372 // We look for, in order of preference: 2373 // 0 AlbumArt.jpg 2374 // 1 AlbumArt*Large.jpg 2375 // 2 Any other jpg image with 'albumart' anywhere in the name 2376 // 3 Any other jpg image 2377 // 4 any other png image 2378 if (compressed == null && path != null) { 2379 int lastSlash = path.lastIndexOf('/'); 2380 if (lastSlash > 0) { 2381 2382 String artPath = path.substring(0, lastSlash); 2383 String sdroot = Environment.getExternalStorageDirectory().getAbsolutePath(); 2384 String dwndir = sdroot + "/download"; // there's no public constant for this 2385 2386 String bestmatch = null; 2387 synchronized (sFolderArtMap) { 2388 if (sFolderArtMap.containsKey(artPath)) { 2389 bestmatch = sFolderArtMap.get(artPath); 2390 } else if (!artPath.equals(sdroot) && 2391 !artPath.equals(dwndir)) { 2392 File dir = new File(artPath); 2393 String [] entrynames = dir.list(); 2394 if (entrynames == null) { 2395 return null; 2396 } 2397 bestmatch = null; 2398 int matchlevel = 1000; 2399 for (int i = entrynames.length - 1; i >=0; i--) { 2400 String entry = entrynames[i].toLowerCase(); 2401 if (entry.equals("albumart.jpg")) { 2402 bestmatch = entrynames[i]; 2403 break; 2404 } else if (entry.startsWith("albumart") 2405 && entry.endsWith("large.jpg") 2406 && matchlevel > 1) { 2407 bestmatch = entrynames[i]; 2408 matchlevel = 1; 2409 } else if (entry.contains("albumart") 2410 && entry.endsWith(".jpg") 2411 && matchlevel > 2) { 2412 bestmatch = entrynames[i]; 2413 matchlevel = 2; 2414 } else if (entry.endsWith(".jpg") && matchlevel > 3) { 2415 bestmatch = entrynames[i]; 2416 matchlevel = 3; 2417 } else if (entry.endsWith(".png") && matchlevel > 4) { 2418 bestmatch = entrynames[i]; 2419 matchlevel = 4; 2420 } 2421 } 2422 // note that this may insert null if no album art was found 2423 sFolderArtMap.put(artPath, bestmatch); 2424 } 2425 } 2426 2427 if (bestmatch != null) { 2428 File file = new File(artPath, bestmatch); 2429 if (file.exists()) { 2430 compressed = new byte[(int)file.length()]; 2431 FileInputStream stream = null; 2432 try { 2433 stream = new FileInputStream(file); 2434 stream.read(compressed); 2435 } catch (IOException ex) { 2436 compressed = null; 2437 } finally { 2438 if (stream != null) { 2439 stream.close(); 2440 } 2441 } 2442 } 2443 } 2444 } 2445 } 2446 } catch (IOException e) { 2447 } 2448 2449 return compressed; 2450 } 2451 2452 // Return a URI to write the album art to and update the database as necessary. 2453 Uri getAlbumArtOutputUri(SQLiteDatabase db, long album_id, Uri albumart_uri) { 2454 Uri out = null; 2455 // TODO: this could be done more efficiently with a call to db.replace(), which 2456 // replaces or inserts as needed, making it unnecessary to query() first. 2457 if (albumart_uri != null) { 2458 Cursor c = query(albumart_uri, new String [] { "_data" }, 2459 null, null, null); 2460 if (c.moveToFirst()) { 2461 String albumart_path = c.getString(0); 2462 if (ensureFileExists(albumart_path)) { 2463 out = albumart_uri; 2464 } 2465 } else { 2466 albumart_uri = null; 2467 } 2468 c.close(); 2469 } 2470 if (albumart_uri == null){ 2471 ContentValues initialValues = new ContentValues(); 2472 initialValues.put("album_id", album_id); 2473 try { 2474 ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 2475 long rowId = db.insert("album_art", "_data", values); 2476 if (rowId > 0) { 2477 out = ContentUris.withAppendedId(ALBUMART_URI, rowId); 2478 } 2479 } catch (IllegalStateException ex) { 2480 Log.e(TAG, "error creating album thumb file"); 2481 } 2482 } 2483 return out; 2484 } 2485 2486 // Write out the album art to the output URI, recompresses the given Bitmap 2487 // if necessary, otherwise writes the compressed data. 2488 private void writeAlbumArt( 2489 boolean need_to_recompress, Uri out, byte[] compressed, Bitmap bm) { 2490 boolean success = false; 2491 try { 2492 OutputStream outstream = getContext().getContentResolver().openOutputStream(out); 2493 2494 if (!need_to_recompress) { 2495 // No need to recompress here, just write out the original 2496 // compressed data here. 2497 outstream.write(compressed); 2498 success = true; 2499 } else { 2500 success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream); 2501 } 2502 2503 outstream.close(); 2504 } catch (FileNotFoundException ex) { 2505 Log.e(TAG, "error creating file", ex); 2506 } catch (IOException ex) { 2507 Log.e(TAG, "error creating file", ex); 2508 } 2509 if (!success) { 2510 // the thumbnail was not written successfully, delete the entry that refers to it 2511 getContext().getContentResolver().delete(out, null, null); 2512 } 2513 } 2514 2515 private ParcelFileDescriptor getThumb(SQLiteDatabase db, String path, long album_id, 2516 Uri albumart_uri) { 2517 ThumbData d = new ThumbData(); 2518 d.db = db; 2519 d.path = path; 2520 d.album_id = album_id; 2521 d.albumart_uri = albumart_uri; 2522 return makeThumbInternal(d); 2523 } 2524 2525 private ParcelFileDescriptor makeThumbInternal(ThumbData d) { 2526 byte[] compressed = getCompressedAlbumArt(getContext(), d.path); 2527 2528 if (compressed == null) { 2529 return null; 2530 } 2531 2532 Bitmap bm = null; 2533 boolean need_to_recompress = true; 2534 2535 try { 2536 // get the size of the bitmap 2537 BitmapFactory.Options opts = new BitmapFactory.Options(); 2538 opts.inJustDecodeBounds = true; 2539 opts.inSampleSize = 1; 2540 BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts); 2541 2542 // request a reasonably sized output image 2543 // TODO: don't hardcode the size 2544 while (opts.outHeight > 320 || opts.outWidth > 320) { 2545 opts.outHeight /= 2; 2546 opts.outWidth /= 2; 2547 opts.inSampleSize *= 2; 2548 } 2549 2550 if (opts.inSampleSize == 1) { 2551 // The original album art was of proper size, we won't have to 2552 // recompress the bitmap later. 2553 need_to_recompress = false; 2554 } else { 2555 // get the image for real now 2556 opts.inJustDecodeBounds = false; 2557 opts.inPreferredConfig = Bitmap.Config.RGB_565; 2558 bm = BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts); 2559 2560 if (bm != null && bm.getConfig() == null) { 2561 Bitmap nbm = bm.copy(Bitmap.Config.RGB_565, false); 2562 if (nbm != null && nbm != bm) { 2563 bm.recycle(); 2564 bm = nbm; 2565 } 2566 } 2567 } 2568 } catch (Exception e) { 2569 } 2570 2571 if (need_to_recompress && bm == null) { 2572 return null; 2573 } 2574 2575 if (d.albumart_uri == null) { 2576 // this one doesn't need to be saved (probably a song with an unknown album), 2577 // so stick it in a memory file and return that 2578 try { 2579 MemoryFile file = new MemoryFile("albumthumb", compressed.length); 2580 file.writeBytes(compressed, 0, 0, compressed.length); 2581 file.deactivate(); 2582 return file.getParcelFileDescriptor(); 2583 } catch (IOException e) { 2584 } 2585 } else { 2586 // This one needs to actually be saved on the sd card. 2587 // This is wrapped in a transaction because there are various things 2588 // that could go wrong while generating the thumbnail, and we only want 2589 // to update the database when all steps succeeded. 2590 d.db.beginTransaction(); 2591 try { 2592 Uri out = getAlbumArtOutputUri(d.db, d.album_id, d.albumart_uri); 2593 2594 if (out != null) { 2595 writeAlbumArt(need_to_recompress, out, compressed, bm); 2596 getContext().getContentResolver().notifyChange(MEDIA_URI, null); 2597 ParcelFileDescriptor pfd = openFileHelper(out, "r"); 2598 d.db.setTransactionSuccessful(); 2599 return pfd; 2600 } 2601 } catch (FileNotFoundException ex) { 2602 // do nothing, just return null below 2603 } catch (UnsupportedOperationException ex) { 2604 // do nothing, just return null below 2605 } finally { 2606 d.db.endTransaction(); 2607 if (bm != null) { 2608 bm.recycle(); 2609 } 2610 } 2611 } 2612 return null; 2613 } 2614 2615 /** 2616 * Look up the artist or album entry for the given name, creating that entry 2617 * if it does not already exists. 2618 * @param db The database 2619 * @param table The table to store the key/name pair in. 2620 * @param keyField The name of the key-column 2621 * @param nameField The name of the name-column 2622 * @param rawName The name that the calling app was trying to insert into the database 2623 * @param cacheName The string that will be inserted in to the cache 2624 * @param path The full path to the file being inserted in to the audio table 2625 * @param albumHash A hash to distinguish between different albums of the same name 2626 * @param artist The name of the artist, if known 2627 * @param cache The cache to add this entry to 2628 * @param srcuri The Uri that prompted the call to this method, used for determining whether this is 2629 * the internal or external database 2630 * @return The row ID for this artist/album, or -1 if the provided name was invalid 2631 */ 2632 private long getKeyIdForName(SQLiteDatabase db, String table, String keyField, String nameField, 2633 String rawName, String cacheName, String path, int albumHash, 2634 String artist, HashMap<String, Long> cache, Uri srcuri) { 2635 long rowId; 2636 2637 if (rawName == null || rawName.length() == 0) { 2638 return -1; 2639 } 2640 String k = MediaStore.Audio.keyFor(rawName); 2641 2642 if (k == null) { 2643 return -1; 2644 } 2645 2646 boolean isAlbum = table.equals("albums"); 2647 boolean isUnknown = MediaStore.UNKNOWN_STRING.equals(rawName); 2648 2649 // To distinguish same-named albums, we append a hash of the path. 2650 // Ideally we would also take things like CDDB ID in to account, so 2651 // we can group files from the same album that aren't in the same 2652 // folder, but this is a quick and easy start that works immediately 2653 // without requiring support from the mp3, mp4 and Ogg meta data 2654 // readers, as long as the albums are in different folders. 2655 if (isAlbum) { 2656 k = k + albumHash; 2657 if (isUnknown) { 2658 k = k + artist; 2659 } 2660 } 2661 2662 String [] selargs = { k }; 2663 Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null); 2664 2665 try { 2666 switch (c.getCount()) { 2667 case 0: { 2668 // insert new entry into table 2669 ContentValues otherValues = new ContentValues(); 2670 otherValues.put(keyField, k); 2671 otherValues.put(nameField, rawName); 2672 rowId = db.insert(table, "duration", otherValues); 2673 if (path != null && isAlbum && ! isUnknown) { 2674 // We just inserted a new album. Now create an album art thumbnail for it. 2675 makeThumbAsync(db, path, rowId); 2676 } 2677 if (rowId > 0) { 2678 String volume = srcuri.toString().substring(16, 24); // extract internal/external 2679 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 2680 getContext().getContentResolver().notifyChange(uri, null); 2681 } 2682 } 2683 break; 2684 case 1: { 2685 // Use the existing entry 2686 c.moveToFirst(); 2687 rowId = c.getLong(0); 2688 2689 // Determine whether the current rawName is better than what's 2690 // currently stored in the table, and update the table if it is. 2691 String currentFancyName = c.getString(2); 2692 String bestName = makeBestName(rawName, currentFancyName); 2693 if (!bestName.equals(currentFancyName)) { 2694 // update the table with the new name 2695 ContentValues newValues = new ContentValues(); 2696 newValues.put(nameField, bestName); 2697 db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null); 2698 String volume = srcuri.toString().substring(16, 24); // extract internal/external 2699 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 2700 getContext().getContentResolver().notifyChange(uri, null); 2701 } 2702 } 2703 break; 2704 default: 2705 // corrupt database 2706 Log.e(TAG, "Multiple entries in table " + table + " for key " + k); 2707 rowId = -1; 2708 break; 2709 } 2710 } finally { 2711 if (c != null) c.close(); 2712 } 2713 2714 if (cache != null && ! isUnknown) { 2715 cache.put(cacheName, rowId); 2716 } 2717 return rowId; 2718 } 2719 2720 /** 2721 * Returns the best string to use for display, given two names. 2722 * Note that this function does not necessarily return either one 2723 * of the provided names; it may decide to return a better alternative 2724 * (for example, specifying the inputs "Police" and "Police, The" will 2725 * return "The Police") 2726 * 2727 * The basic assumptions are: 2728 * - longer is better ("The police" is better than "Police") 2729 * - prefix is better ("The Police" is better than "Police, The") 2730 * - accents are better ("Motörhead" is better than "Motorhead") 2731 * 2732 * @param one The first of the two names to consider 2733 * @param two The last of the two names to consider 2734 * @return The actual name to use 2735 */ 2736 String makeBestName(String one, String two) { 2737 String name; 2738 2739 // Longer names are usually better. 2740 if (one.length() > two.length()) { 2741 name = one; 2742 } else { 2743 // Names with accents are usually better, and conveniently sort later 2744 if (one.toLowerCase().compareTo(two.toLowerCase()) > 0) { 2745 name = one; 2746 } else { 2747 name = two; 2748 } 2749 } 2750 2751 // Prefixes are better than postfixes. 2752 if (name.endsWith(", the") || name.endsWith(",the") || 2753 name.endsWith(", an") || name.endsWith(",an") || 2754 name.endsWith(", a") || name.endsWith(",a")) { 2755 String fix = name.substring(1 + name.lastIndexOf(',')); 2756 name = fix.trim() + " " + name.substring(0, name.lastIndexOf(',')); 2757 } 2758 2759 // TODO: word-capitalize the resulting name 2760 return name; 2761 } 2762 2763 2764 /** 2765 * Looks up the database based on the given URI. 2766 * 2767 * @param uri The requested URI 2768 * @returns the database for the given URI 2769 */ 2770 private DatabaseHelper getDatabaseForUri(Uri uri) { 2771 synchronized (mDatabases) { 2772 if (uri.getPathSegments().size() > 1) { 2773 return mDatabases.get(uri.getPathSegments().get(0)); 2774 } 2775 } 2776 return null; 2777 } 2778 2779 /** 2780 * Attach the database for a volume (internal or external). 2781 * Does nothing if the volume is already attached, otherwise 2782 * checks the volume ID and sets up the corresponding database. 2783 * 2784 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}. 2785 * @return the content URI of the attached volume. 2786 */ 2787 private Uri attachVolume(String volume) { 2788 if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) { 2789 throw new SecurityException( 2790 "Opening and closing databases not allowed."); 2791 } 2792 2793 synchronized (mDatabases) { 2794 if (mDatabases.get(volume) != null) { // Already attached 2795 return Uri.parse("content://media/" + volume); 2796 } 2797 2798 DatabaseHelper db; 2799 if (INTERNAL_VOLUME.equals(volume)) { 2800 db = new DatabaseHelper(getContext(), INTERNAL_DATABASE_NAME, true); 2801 } else if (EXTERNAL_VOLUME.equals(volume)) { 2802 String path = Environment.getExternalStorageDirectory().getPath(); 2803 int volumeID = FileUtils.getFatVolumeId(path); 2804 if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID); 2805 2806 // generate database name based on volume ID 2807 String dbName = "external-" + Integer.toHexString(volumeID) + ".db"; 2808 db = new DatabaseHelper(getContext(), dbName, false); 2809 mVolumeId = volumeID; 2810 } else { 2811 throw new IllegalArgumentException("There is no volume named " + volume); 2812 } 2813 2814 mDatabases.put(volume, db); 2815 2816 if (!db.mInternal) { 2817 // clean up stray album art files: delete every file not in the database 2818 File[] files = new File( 2819 Environment.getExternalStorageDirectory(), 2820 ALBUM_THUMB_FOLDER).listFiles(); 2821 HashSet<String> fileSet = new HashSet(); 2822 for (int i = 0; files != null && i < files.length; i++) { 2823 fileSet.add(files[i].getPath()); 2824 } 2825 2826 Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, 2827 new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null); 2828 try { 2829 while (cursor != null && cursor.moveToNext()) { 2830 fileSet.remove(cursor.getString(0)); 2831 } 2832 } finally { 2833 if (cursor != null) cursor.close(); 2834 } 2835 2836 Iterator<String> iterator = fileSet.iterator(); 2837 while (iterator.hasNext()) { 2838 String filename = iterator.next(); 2839 if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename); 2840 new File(filename).delete(); 2841 } 2842 } 2843 } 2844 2845 if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume); 2846 return Uri.parse("content://media/" + volume); 2847 } 2848 2849 /** 2850 * Detach the database for a volume (must be external). 2851 * Does nothing if the volume is already detached, otherwise 2852 * closes the database and sends a notification to listeners. 2853 * 2854 * @param uri The content URI of the volume, as returned by {@link #attachVolume} 2855 */ 2856 private void detachVolume(Uri uri) { 2857 if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) { 2858 throw new SecurityException( 2859 "Opening and closing databases not allowed."); 2860 } 2861 2862 String volume = uri.getPathSegments().get(0); 2863 if (INTERNAL_VOLUME.equals(volume)) { 2864 throw new UnsupportedOperationException( 2865 "Deleting the internal volume is not allowed"); 2866 } else if (!EXTERNAL_VOLUME.equals(volume)) { 2867 throw new IllegalArgumentException( 2868 "There is no volume named " + volume); 2869 } 2870 2871 synchronized (mDatabases) { 2872 DatabaseHelper database = mDatabases.get(volume); 2873 if (database == null) return; 2874 2875 try { 2876 // touch the database file to show it is most recently used 2877 File file = new File(database.getReadableDatabase().getPath()); 2878 file.setLastModified(System.currentTimeMillis()); 2879 } catch (SQLException e) { 2880 Log.e(TAG, "Can't touch database file", e); 2881 } 2882 2883 mDatabases.remove(volume); 2884 database.close(); 2885 } 2886 2887 getContext().getContentResolver().notifyChange(uri, null); 2888 if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume); 2889 } 2890 2891 private static String TAG = "MediaProvider"; 2892 private static final boolean LOCAL_LOGV = true; 2893 private static final int DATABASE_VERSION = 80; 2894 private static final String INTERNAL_DATABASE_NAME = "internal.db"; 2895 2896 // maximum number of cached external databases to keep 2897 private static final int MAX_EXTERNAL_DATABASES = 3; 2898 2899 // Delete databases that have not been used in two months 2900 // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60) 2901 private static final long OBSOLETE_DATABASE_DB = 5184000000L; 2902 2903 private HashMap<String, DatabaseHelper> mDatabases; 2904 2905 private Worker mThumbWorker; 2906 private Handler mThumbHandler; 2907 2908 // name of the volume currently being scanned by the media scanner (or null) 2909 private String mMediaScannerVolume; 2910 2911 // current FAT volume ID 2912 private int mVolumeId; 2913 2914 static final String INTERNAL_VOLUME = "internal"; 2915 static final String EXTERNAL_VOLUME = "external"; 2916 static final String ALBUM_THUMB_FOLDER = "Android/data/com.android.providers.media/albumthumbs"; 2917 2918 // path for writing contents of in memory temp database 2919 private String mTempDatabasePath; 2920 2921 private static final int IMAGES_MEDIA = 1; 2922 private static final int IMAGES_MEDIA_ID = 2; 2923 private static final int IMAGES_THUMBNAILS = 3; 2924 private static final int IMAGES_THUMBNAILS_ID = 4; 2925 2926 private static final int AUDIO_MEDIA = 100; 2927 private static final int AUDIO_MEDIA_ID = 101; 2928 private static final int AUDIO_MEDIA_ID_GENRES = 102; 2929 private static final int AUDIO_MEDIA_ID_GENRES_ID = 103; 2930 private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104; 2931 private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105; 2932 private static final int AUDIO_GENRES = 106; 2933 private static final int AUDIO_GENRES_ID = 107; 2934 private static final int AUDIO_GENRES_ID_MEMBERS = 108; 2935 private static final int AUDIO_GENRES_ID_MEMBERS_ID = 109; 2936 private static final int AUDIO_PLAYLISTS = 110; 2937 private static final int AUDIO_PLAYLISTS_ID = 111; 2938 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 2939 private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113; 2940 private static final int AUDIO_ARTISTS = 114; 2941 private static final int AUDIO_ARTISTS_ID = 115; 2942 private static final int AUDIO_ALBUMS = 116; 2943 private static final int AUDIO_ALBUMS_ID = 117; 2944 private static final int AUDIO_ARTISTS_ID_ALBUMS = 118; 2945 private static final int AUDIO_ALBUMART = 119; 2946 private static final int AUDIO_ALBUMART_ID = 120; 2947 private static final int AUDIO_ALBUMART_FILE_ID = 121; 2948 2949 private static final int VIDEO_MEDIA = 200; 2950 private static final int VIDEO_MEDIA_ID = 201; 2951 private static final int VIDEO_THUMBNAILS = 202; 2952 private static final int VIDEO_THUMBNAILS_ID = 203; 2953 2954 private static final int VOLUMES = 300; 2955 private static final int VOLUMES_ID = 301; 2956 2957 private static final int AUDIO_SEARCH_LEGACY = 400; 2958 private static final int AUDIO_SEARCH_BASIC = 401; 2959 private static final int AUDIO_SEARCH_FANCY = 402; 2960 2961 private static final int MEDIA_SCANNER = 500; 2962 2963 private static final int FS_ID = 600; 2964 2965 private static final UriMatcher URI_MATCHER = 2966 new UriMatcher(UriMatcher.NO_MATCH); 2967 2968 private static final String[] ID_PROJECTION = new String[] { 2969 MediaStore.MediaColumns._ID 2970 }; 2971 2972 private static final String[] MIME_TYPE_PROJECTION = new String[] { 2973 MediaStore.MediaColumns._ID, // 0 2974 MediaStore.MediaColumns.MIME_TYPE, // 1 2975 }; 2976 2977 private static final String[] READY_FLAG_PROJECTION = new String[] { 2978 MediaStore.MediaColumns._ID, 2979 MediaStore.MediaColumns.DATA, 2980 Images.Media.MINI_THUMB_MAGIC 2981 }; 2982 2983 private static final String[] EXTERNAL_DATABASE_TABLES = new String[] { 2984 "images", 2985 "thumbnails", 2986 "audio_meta", 2987 "artists", 2988 "albums", 2989 "audio_genres", 2990 "audio_genres_map", 2991 "audio_playlists", 2992 "audio_playlists_map", 2993 "video", 2994 }; 2995 2996 static 2997 { 2998 URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA); 2999 URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID); 3000 URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS); 3001 URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID); 3002 3003 URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA); 3004 URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID); 3005 URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES); 3006 URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID); 3007 URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS); 3008 URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID); 3009 URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES); 3010 URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID); 3011 URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS); 3012 URI_MATCHER.addURI("media", "*/audio/genres/#/members/#", AUDIO_GENRES_ID_MEMBERS_ID); 3013 URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS); 3014 URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID); 3015 URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS); 3016 URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID); 3017 URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS); 3018 URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID); 3019 URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS); 3020 URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS); 3021 URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID); 3022 URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART); 3023 URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID); 3024 URI_MATCHER.addURI("media", "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID); 3025 3026 URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA); 3027 URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID); 3028 URI_MATCHER.addURI("media", "*/video/thumbnails", VIDEO_THUMBNAILS); 3029 URI_MATCHER.addURI("media", "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID); 3030 3031 URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER); 3032 3033 URI_MATCHER.addURI("media", "*/fs_id", FS_ID); 3034 3035 URI_MATCHER.addURI("media", "*", VOLUMES_ID); 3036 URI_MATCHER.addURI("media", null, VOLUMES); 3037 3038 /** 3039 * @deprecated use the 'basic' or 'fancy' search Uris instead 3040 */ 3041 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY, 3042 AUDIO_SEARCH_LEGACY); 3043 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 3044 AUDIO_SEARCH_LEGACY); 3045 3046 // used for search suggestions 3047 URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY, 3048 AUDIO_SEARCH_BASIC); 3049 URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY + 3050 "/*", AUDIO_SEARCH_BASIC); 3051 3052 // used by the music app's search activity 3053 URI_MATCHER.addURI("media", "*/audio/search/fancy", AUDIO_SEARCH_FANCY); 3054 URI_MATCHER.addURI("media", "*/audio/search/fancy/*", AUDIO_SEARCH_FANCY); 3055 } 3056} 3057