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