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