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