MediaProvider.java revision ae62a1d602e7ed2e0e30e271bddbb27aa71469f6
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 if (fromVersion < 78) { 788 // Force a rescan of the images/video entries so we can update 789 // latest changed DATE_TAKEN units (in milliseconds). 790 db.execSQL("UPDATE images SET date_modified=0;"); 791 db.execSQL("UPDATE video SET date_modified=0;"); 792 } 793 } 794 795 private static void recreateAudioView(SQLiteDatabase db) { 796 // Provides a unified audio/artist/album info view. 797 // Note that views are read-only, so we define a trigger to allow deletes. 798 db.execSQL("DROP VIEW IF EXISTS audio"); 799 db.execSQL("DROP TRIGGER IF EXISTS audio_delete"); 800 db.execSQL("CREATE VIEW IF NOT EXISTS audio as SELECT * FROM audio_meta " + 801 "LEFT OUTER JOIN artists ON audio_meta.artist_id=artists.artist_id " + 802 "LEFT OUTER JOIN albums ON audio_meta.album_id=albums.album_id;"); 803 804 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_delete INSTEAD OF DELETE ON audio " + 805 "BEGIN " + 806 "DELETE from audio_meta where _id=old._id;" + 807 "DELETE from audio_playlists_map where audio_id=old._id;" + 808 "DELETE from audio_genres_map where audio_id=old._id;" + 809 "END"); 810 } 811 812 /** 813 * Iterate through the rows of a table in a database, ensuring that the bucket_id and 814 * bucket_display_name columns are correct. 815 * @param db 816 * @param tableName 817 */ 818 private static void updateBucketNames(SQLiteDatabase db, String tableName) { 819 // Rebuild the bucket_display_name column using the natural case rather than lower case. 820 db.beginTransaction(); 821 try { 822 String[] columns = {BaseColumns._ID, MediaColumns.DATA}; 823 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 824 try { 825 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 826 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 827 while (cursor.moveToNext()) { 828 String data = cursor.getString(dataColumnIndex); 829 ContentValues values = new ContentValues(); 830 computeBucketValues(data, values); 831 int rowId = cursor.getInt(idColumnIndex); 832 db.update(tableName, values, "_id=" + rowId, null); 833 } 834 } finally { 835 cursor.close(); 836 } 837 db.setTransactionSuccessful(); 838 } finally { 839 db.endTransaction(); 840 } 841 } 842 843 /** 844 * Iterate through the rows of a table in a database, ensuring that the 845 * display name column has a value. 846 * @param db 847 * @param tableName 848 */ 849 private static void updateDisplayName(SQLiteDatabase db, String tableName) { 850 // Fill in default values for null displayName values 851 db.beginTransaction(); 852 try { 853 String[] columns = {BaseColumns._ID, MediaColumns.DATA, MediaColumns.DISPLAY_NAME}; 854 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 855 try { 856 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 857 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 858 final int displayNameIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME); 859 ContentValues values = new ContentValues(); 860 while (cursor.moveToNext()) { 861 String displayName = cursor.getString(displayNameIndex); 862 if (displayName == null) { 863 String data = cursor.getString(dataColumnIndex); 864 values.clear(); 865 computeDisplayName(data, values); 866 int rowId = cursor.getInt(idColumnIndex); 867 db.update(tableName, values, "_id=" + rowId, null); 868 } 869 } 870 } finally { 871 cursor.close(); 872 } 873 db.setTransactionSuccessful(); 874 } finally { 875 db.endTransaction(); 876 } 877 } 878 /** 879 * @param data The input path 880 * @param values the content values, where the bucked id name and bucket display name are updated. 881 * 882 */ 883 884 private static void computeBucketValues(String data, ContentValues values) { 885 File parentFile = new File(data).getParentFile(); 886 if (parentFile == null) { 887 parentFile = new File("/"); 888 } 889 890 // Lowercase the path for hashing. This avoids duplicate buckets if the 891 // filepath case is changed externally. 892 // Keep the original case for display. 893 String path = parentFile.toString().toLowerCase(); 894 String name = parentFile.getName(); 895 896 // Note: the BUCKET_ID and BUCKET_DISPLAY_NAME attributes are spelled the 897 // same for both images and video. However, for backwards-compatibility reasons 898 // there is no common base class. We use the ImageColumns version here 899 values.put(ImageColumns.BUCKET_ID, path.hashCode()); 900 values.put(ImageColumns.BUCKET_DISPLAY_NAME, name); 901 } 902 903 /** 904 * @param data The input path 905 * @param values the content values, where the display name is updated. 906 * 907 */ 908 private static void computeDisplayName(String data, ContentValues values) { 909 String s = (data == null ? "" : data.toString()); 910 int idx = s.lastIndexOf('/'); 911 if (idx >= 0) { 912 s = s.substring(idx + 1); 913 } 914 values.put("_display_name", s); 915 } 916 917 /** 918 * This method blocks until thumbnail is ready. 919 * 920 * @param thumbUri 921 * @return 922 */ 923 private boolean waitForThumbnailReady(Uri origUri) { 924 Cursor c = this.query(origUri, new String[] { ImageColumns._ID, ImageColumns.DATA, 925 ImageColumns.MINI_THUMB_MAGIC}, null, null, null); 926 if (c == null) return false; 927 928 boolean result = false; 929 930 if (c.moveToFirst()) { 931 long id = c.getLong(0); 932 String path = c.getString(1); 933 long magic = c.getLong(2); 934 935 if (magic == 0 || MiniThumbFile.instance(origUri).getMagic(id) != magic) { 936 MediaThumbRequest req = requestMediaThumbnail(path, origUri, 937 MediaThumbRequest.PRIORITY_HIGH); 938 synchronized (req) { 939 try { 940 while (req.mState == MediaThumbRequest.State.WAIT) { 941 req.wait(); 942 } 943 } catch (InterruptedException e) { 944 Log.w(TAG, e); 945 } 946 if (req.mState == MediaThumbRequest.State.DONE) { 947 result = true; 948 } 949 } 950 } else { 951 result = true; 952 } 953 } 954 c.close(); 955 956 return result; 957 } 958 959 private boolean queryThumbnail(SQLiteQueryBuilder qb, Uri uri, String table, 960 String column, boolean hasThumbnailId) { 961 qb.setTables(table); 962 if (hasThumbnailId) { 963 // For uri dispatched to this method, the 4th path segment is always 964 // the thumbnail id. 965 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 966 // client already knows which thumbnail it wants, bypass it. 967 return true; 968 } 969 String origId = uri.getQueryParameter("orig_id"); 970 // We can't query ready_flag unless we know original id 971 if (origId == null) { 972 // this could be thumbnail query for other purpose, bypass it. 973 return true; 974 } 975 976 boolean needBlocking = "1".equals(uri.getQueryParameter("blocking")); 977 boolean cancelRequest = "1".equals(uri.getQueryParameter("cancel")); 978 Uri origUri = Uri.parse("content://media" + 979 uri.getPath().replaceFirst("thumbnails", "media") + "/" + origId); 980 981 if (needBlocking && !waitForThumbnailReady(origUri)) { 982 Log.w(TAG, "original media doesn't exist or it's canceled."); 983 return false; 984 } else if (cancelRequest) { 985 int pid = Binder.getCallingPid(); 986 long id = -1; 987 try { 988 id = Long.parseLong(origId); 989 } catch (NumberFormatException ex) { 990 // invalid cancel request 991 return false; 992 } 993 boolean cancelAll = (id == -1); 994 synchronized (mMediaThumbQueue) { 995 if (mCurrentThumbRequest != null && mCurrentThumbRequest.mCallingPid == pid && 996 (cancelAll || mCurrentThumbRequest.mOrigId == id)) { 997 synchronized (mCurrentThumbRequest) { 998 mCurrentThumbRequest.mState = MediaThumbRequest.State.CANCEL; 999 mCurrentThumbRequest.notifyAll(); 1000 } 1001 } 1002 for (MediaThumbRequest mtq : mMediaThumbQueue) { 1003 if (mtq.mCallingPid == pid && (cancelAll || mCurrentThumbRequest.mOrigId == id)) { 1004 synchronized (mtq) { 1005 mtq.mState = MediaThumbRequest.State.CANCEL; 1006 mtq.notifyAll(); 1007 } 1008 1009 mMediaThumbQueue.remove(mtq); 1010 } 1011 } 1012 } 1013 } 1014 1015 if (origId != null) { 1016 qb.appendWhere(column + " = " + origId); 1017 } 1018 return true; 1019 } 1020 @SuppressWarnings("fallthrough") 1021 @Override 1022 public Cursor query(Uri uri, String[] projectionIn, String selection, 1023 String[] selectionArgs, String sort) { 1024 int table = URI_MATCHER.match(uri); 1025 1026 // Log.v(TAG, "query: uri="+uri+", selection="+selection); 1027 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1028 if (table == MEDIA_SCANNER) { 1029 if (mMediaScannerVolume == null) { 1030 return null; 1031 } else { 1032 // create a cursor to return volume currently being scanned by the media scanner 1033 return new MediaScannerCursor(mMediaScannerVolume); 1034 } 1035 } 1036 1037 String groupBy = null; 1038 DatabaseHelper database = getDatabaseForUri(uri); 1039 if (database == null) { 1040 return null; 1041 } 1042 SQLiteDatabase db = database.getReadableDatabase(); 1043 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1044 String limit = uri.getQueryParameter("limit"); 1045 boolean hasThumbnailId = false; 1046 1047 switch (table) { 1048 case IMAGES_MEDIA: 1049 qb.setTables("images"); 1050 if (uri.getQueryParameter("distinct") != null) 1051 qb.setDistinct(true); 1052 1053 // set the project map so that data dir is prepended to _data. 1054 //qb.setProjectionMap(mImagesProjectionMap, true); 1055 break; 1056 1057 case IMAGES_MEDIA_ID: 1058 qb.setTables("images"); 1059 if (uri.getQueryParameter("distinct") != null) 1060 qb.setDistinct(true); 1061 1062 // set the project map so that data dir is prepended to _data. 1063 //qb.setProjectionMap(mImagesProjectionMap, true); 1064 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 1065 break; 1066 1067 case IMAGES_THUMBNAILS_ID: 1068 hasThumbnailId = true; 1069 case IMAGES_THUMBNAILS: 1070 if (!queryThumbnail(qb, uri, "thumbnails", "image_id", hasThumbnailId)) { 1071 return null; 1072 } 1073 break; 1074 1075 case AUDIO_MEDIA: 1076 qb.setTables("audio "); 1077 break; 1078 1079 case AUDIO_MEDIA_ID: 1080 qb.setTables("audio"); 1081 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1082 break; 1083 1084 case AUDIO_MEDIA_ID_GENRES: 1085 qb.setTables("audio_genres"); 1086 qb.appendWhere("_id IN (SELECT genre_id FROM " + 1087 "audio_genres_map WHERE audio_id = " + 1088 uri.getPathSegments().get(3) + ")"); 1089 break; 1090 1091 case AUDIO_MEDIA_ID_GENRES_ID: 1092 qb.setTables("audio_genres"); 1093 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1094 break; 1095 1096 case AUDIO_MEDIA_ID_PLAYLISTS: 1097 qb.setTables("audio_playlists"); 1098 qb.appendWhere("_id IN (SELECT playlist_id FROM " + 1099 "audio_playlists_map WHERE audio_id = " + 1100 uri.getPathSegments().get(3) + ")"); 1101 break; 1102 1103 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1104 qb.setTables("audio_playlists"); 1105 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1106 break; 1107 1108 case AUDIO_GENRES: 1109 qb.setTables("audio_genres"); 1110 break; 1111 1112 case AUDIO_GENRES_ID: 1113 qb.setTables("audio_genres"); 1114 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1115 break; 1116 1117 case AUDIO_GENRES_ID_MEMBERS: 1118 qb.setTables("audio"); 1119 qb.appendWhere("_id IN (SELECT audio_id FROM " + 1120 "audio_genres_map WHERE genre_id = " + 1121 uri.getPathSegments().get(3) + ")"); 1122 break; 1123 1124 case AUDIO_GENRES_ID_MEMBERS_ID: 1125 qb.setTables("audio"); 1126 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1127 break; 1128 1129 case AUDIO_PLAYLISTS: 1130 qb.setTables("audio_playlists"); 1131 break; 1132 1133 case AUDIO_PLAYLISTS_ID: 1134 qb.setTables("audio_playlists"); 1135 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1136 break; 1137 1138 case AUDIO_PLAYLISTS_ID_MEMBERS: 1139 if (projectionIn != null) { 1140 for (int i = 0; i < projectionIn.length; i++) { 1141 if (projectionIn[i].equals("_id")) { 1142 projectionIn[i] = "audio_playlists_map._id AS _id"; 1143 } 1144 } 1145 } 1146 qb.setTables("audio_playlists_map, audio"); 1147 qb.appendWhere("audio._id = audio_id AND playlist_id = " 1148 + uri.getPathSegments().get(3)); 1149 break; 1150 1151 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1152 qb.setTables("audio"); 1153 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 1154 break; 1155 1156 case VIDEO_MEDIA: 1157 qb.setTables("video"); 1158 break; 1159 1160 case VIDEO_MEDIA_ID: 1161 qb.setTables("video"); 1162 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1163 break; 1164 1165 case VIDEO_THUMBNAILS_ID: 1166 hasThumbnailId = true; 1167 case VIDEO_THUMBNAILS: 1168 if (!queryThumbnail(qb, uri, "videothumbnails", "video_id", hasThumbnailId)) { 1169 return null; 1170 } 1171 break; 1172 1173 case AUDIO_ARTISTS: 1174 qb.setTables("artist_info"); 1175 break; 1176 1177 case AUDIO_ARTISTS_ID: 1178 qb.setTables("artist_info"); 1179 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1180 break; 1181 1182 case AUDIO_ARTISTS_ID_ALBUMS: 1183 String aid = uri.getPathSegments().get(3); 1184 qb.setTables("audio LEFT OUTER JOIN album_art ON" + 1185 " audio.album_id=album_art.album_id"); 1186 qb.appendWhere("is_music=1 AND audio.album_id IN (SELECT album_id FROM " + 1187 "artists_albums_map WHERE artist_id = " + 1188 aid + ")"); 1189 groupBy = "audio.album_id"; 1190 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 1191 "count(CASE WHEN artist_id==" + aid + " THEN 'foo' ELSE NULL END) AS " + 1192 MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 1193 qb.setProjectionMap(sArtistAlbumsMap); 1194 break; 1195 1196 case AUDIO_ALBUMS: 1197 qb.setTables("album_info"); 1198 break; 1199 1200 case AUDIO_ALBUMS_ID: 1201 qb.setTables("album_info"); 1202 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 1203 break; 1204 1205 case AUDIO_ALBUMART_ID: 1206 qb.setTables("album_art"); 1207 qb.appendWhere("album_id=" + uri.getPathSegments().get(3)); 1208 break; 1209 1210 case AUDIO_SEARCH_LEGACY: 1211 Log.w(TAG, "Legacy media search Uri used. Please update your code."); 1212 // fall through 1213 case AUDIO_SEARCH_FANCY: 1214 case AUDIO_SEARCH_BASIC: 1215 return doAudioSearch(db, qb, uri, projectionIn, selection, selectionArgs, sort, 1216 table, limit); 1217 1218 default: 1219 throw new IllegalStateException("Unknown URL: " + uri.toString()); 1220 } 1221 1222 // Log.v(TAG, "query = "+ qb.buildQuery(projectionIn, selection, selectionArgs, groupBy, null, sort, limit)); 1223 Cursor c = qb.query(db, projectionIn, selection, 1224 selectionArgs, groupBy, null, sort, limit); 1225 1226 if (c != null) { 1227 c.setNotificationUri(getContext().getContentResolver(), uri); 1228 } 1229 1230 return c; 1231 } 1232 1233 private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb, 1234 Uri uri, String[] projectionIn, String selection, 1235 String[] selectionArgs, String sort, int mode, 1236 String limit) { 1237 1238 String mSearchString = uri.toString().endsWith("/") ? "" : uri.getLastPathSegment(); 1239 mSearchString = mSearchString.replaceAll(" ", " ").trim().toLowerCase(); 1240 1241 String [] searchWords = mSearchString.length() > 0 ? 1242 mSearchString.split(" ") : new String[0]; 1243 String [] wildcardWords = new String[searchWords.length]; 1244 Collator col = Collator.getInstance(); 1245 col.setStrength(Collator.PRIMARY); 1246 int len = searchWords.length; 1247 for (int i = 0; i < len; i++) { 1248 // Because we match on individual words here, we need to remove words 1249 // like 'a' and 'the' that aren't part of the keys. 1250 wildcardWords[i] = 1251 (searchWords[i].equals("a") || searchWords[i].equals("an") || 1252 searchWords[i].equals("the")) ? "%" : 1253 '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%'; 1254 } 1255 1256 String where = ""; 1257 for (int i = 0; i < searchWords.length; i++) { 1258 if (i == 0) { 1259 where = "match LIKE ?"; 1260 } else { 1261 where += " AND match LIKE ?"; 1262 } 1263 } 1264 1265 qb.setTables("search"); 1266 String [] cols; 1267 if (mode == AUDIO_SEARCH_FANCY) { 1268 cols = mSearchColsFancy; 1269 } else if (mode == AUDIO_SEARCH_BASIC) { 1270 cols = mSearchColsBasic; 1271 } else { 1272 cols = mSearchColsLegacy; 1273 } 1274 return qb.query(db, cols, where, wildcardWords, null, null, null, limit); 1275 } 1276 1277 @Override 1278 public String getType(Uri url) 1279 { 1280 switch (URI_MATCHER.match(url)) { 1281 case IMAGES_MEDIA_ID: 1282 case AUDIO_MEDIA_ID: 1283 case AUDIO_GENRES_ID_MEMBERS_ID: 1284 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1285 case VIDEO_MEDIA_ID: 1286 Cursor c = query(url, MIME_TYPE_PROJECTION, null, null, null); 1287 if (c != null && c.getCount() == 1) { 1288 c.moveToFirst(); 1289 String mimeType = c.getString(1); 1290 c.deactivate(); 1291 return mimeType; 1292 } 1293 break; 1294 1295 case IMAGES_MEDIA: 1296 case IMAGES_THUMBNAILS: 1297 return Images.Media.CONTENT_TYPE; 1298 case IMAGES_THUMBNAILS_ID: 1299 return "image/jpeg"; 1300 1301 case AUDIO_MEDIA: 1302 case AUDIO_GENRES_ID_MEMBERS: 1303 case AUDIO_PLAYLISTS_ID_MEMBERS: 1304 return Audio.Media.CONTENT_TYPE; 1305 1306 case AUDIO_GENRES: 1307 case AUDIO_MEDIA_ID_GENRES: 1308 return Audio.Genres.CONTENT_TYPE; 1309 case AUDIO_GENRES_ID: 1310 case AUDIO_MEDIA_ID_GENRES_ID: 1311 return Audio.Genres.ENTRY_CONTENT_TYPE; 1312 case AUDIO_PLAYLISTS: 1313 case AUDIO_MEDIA_ID_PLAYLISTS: 1314 return Audio.Playlists.CONTENT_TYPE; 1315 case AUDIO_PLAYLISTS_ID: 1316 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1317 return Audio.Playlists.ENTRY_CONTENT_TYPE; 1318 1319 case VIDEO_MEDIA: 1320 return Video.Media.CONTENT_TYPE; 1321 } 1322 throw new IllegalStateException("Unknown URL"); 1323 } 1324 1325 /** 1326 * Ensures there is a file in the _data column of values, if one isn't 1327 * present a new file is created. 1328 * 1329 * @param initialValues the values passed to insert by the caller 1330 * @return the new values 1331 */ 1332 private ContentValues ensureFile(boolean internal, ContentValues initialValues, 1333 String preferredExtension, String directoryName) { 1334 ContentValues values; 1335 String file = initialValues.getAsString("_data"); 1336 if (TextUtils.isEmpty(file)) { 1337 file = generateFileName(internal, preferredExtension, directoryName); 1338 values = new ContentValues(initialValues); 1339 values.put("_data", file); 1340 } else { 1341 values = initialValues; 1342 } 1343 1344 if (!ensureFileExists(file)) { 1345 throw new IllegalStateException("Unable to create new file: " + file); 1346 } 1347 return values; 1348 } 1349 1350 @Override 1351 public int bulkInsert(Uri uri, ContentValues values[]) { 1352 int match = URI_MATCHER.match(uri); 1353 if (match == VOLUMES) { 1354 return super.bulkInsert(uri, values); 1355 } 1356 DatabaseHelper database = getDatabaseForUri(uri); 1357 if (database == null) { 1358 throw new UnsupportedOperationException( 1359 "Unknown URI: " + uri); 1360 } 1361 SQLiteDatabase db = database.getWritableDatabase(); 1362 db.beginTransaction(); 1363 int numInserted = 0; 1364 try { 1365 int len = values.length; 1366 for (int i = 0; i < len; i++) { 1367 insertInternal(uri, values[i]); 1368 } 1369 numInserted = len; 1370 db.setTransactionSuccessful(); 1371 } finally { 1372 db.endTransaction(); 1373 } 1374 getContext().getContentResolver().notifyChange(uri, null); 1375 return numInserted; 1376 } 1377 1378 @Override 1379 public Uri insert(Uri uri, ContentValues initialValues) 1380 { 1381 Uri newUri = insertInternal(uri, initialValues); 1382 if (newUri != null) { 1383 getContext().getContentResolver().notifyChange(uri, null); 1384 } 1385 return newUri; 1386 } 1387 1388 private Uri insertInternal(Uri uri, ContentValues initialValues) { 1389 long rowId; 1390 int match = URI_MATCHER.match(uri); 1391 1392 // Log.v(TAG, "insertInternal: "+uri+", initValues="+initialValues); 1393 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1394 if (match == MEDIA_SCANNER) { 1395 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 1396 return MediaStore.getMediaScannerUri(); 1397 } 1398 1399 Uri newUri = null; 1400 DatabaseHelper database = getDatabaseForUri(uri); 1401 if (database == null && match != VOLUMES) { 1402 throw new UnsupportedOperationException( 1403 "Unknown URI: " + uri); 1404 } 1405 SQLiteDatabase db = (match == VOLUMES ? null : database.getWritableDatabase()); 1406 1407 if (initialValues == null) { 1408 initialValues = new ContentValues(); 1409 } 1410 1411 switch (match) { 1412 case IMAGES_MEDIA: { 1413 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/Camera"); 1414 1415 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1416 String data = values.getAsString(MediaColumns.DATA); 1417 if (! values.containsKey(MediaColumns.DISPLAY_NAME)) { 1418 computeDisplayName(data, values); 1419 } 1420 computeBucketValues(data, values); 1421 rowId = db.insert("images", "name", values); 1422 1423 if (rowId > 0) { 1424 newUri = ContentUris.withAppendedId( 1425 Images.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 1426 requestMediaThumbnail(data, newUri, MediaThumbRequest.PRIORITY_NORMAL); 1427 } 1428 break; 1429 } 1430 1431 // This will be triggered by requestMediaThumbnail (see getThumbnailUri) 1432 case IMAGES_THUMBNAILS: { 1433 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", 1434 "DCIM/.thumbnails"); 1435 rowId = db.insert("thumbnails", "name", values); 1436 if (rowId > 0) { 1437 newUri = ContentUris.withAppendedId(Images.Thumbnails. 1438 getContentUri(uri.getPathSegments().get(0)), rowId); 1439 } 1440 break; 1441 } 1442 1443 // This is currently only used by MICRO_KIND video thumbnail (see getThumbnailUri) 1444 case VIDEO_THUMBNAILS: { 1445 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", 1446 "DCIM/.thumbnails"); 1447 rowId = db.insert("videothumbnails", "name", values); 1448 if (rowId > 0) { 1449 newUri = ContentUris.withAppendedId(Video.Thumbnails. 1450 getContentUri(uri.getPathSegments().get(0)), rowId); 1451 } 1452 break; 1453 } 1454 1455 case AUDIO_MEDIA: { 1456 // SQLite Views are read-only, so we need to deconstruct this 1457 // insert and do inserts into the underlying tables. 1458 // If doing this here turns out to be a performance bottleneck, 1459 // consider moving this to native code and using triggers on 1460 // the view. 1461 ContentValues values = new ContentValues(initialValues); 1462 1463 // Insert the artist into the artist table and remove it from 1464 // the input values 1465 Object so = values.get("artist"); 1466 String s = (so == null ? "" : so.toString()); 1467 values.remove("artist"); 1468 long artistRowId; 1469 HashMap<String, Long> artistCache = database.mArtistCache; 1470 String path = values.getAsString("_data"); 1471 synchronized(artistCache) { 1472 Long temp = artistCache.get(s); 1473 if (temp == null) { 1474 artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist", 1475 s, s, path, 0, null, artistCache, uri); 1476 } else { 1477 artistRowId = temp.longValue(); 1478 } 1479 } 1480 String artist = s; 1481 1482 // Do the same for the album field 1483 so = values.get("album"); 1484 s = (so == null ? "" : so.toString()); 1485 values.remove("album"); 1486 long albumRowId; 1487 HashMap<String, Long> albumCache = database.mAlbumCache; 1488 synchronized(albumCache) { 1489 int albumhash = path.substring(0, path.lastIndexOf('/')).hashCode(); 1490 String cacheName = s + albumhash; 1491 Long temp = albumCache.get(cacheName); 1492 if (temp == null) { 1493 albumRowId = getKeyIdForName(db, "albums", "album_key", "album", 1494 s, cacheName, path, albumhash, artist, albumCache, uri); 1495 } else { 1496 albumRowId = temp; 1497 } 1498 } 1499 1500 values.put("artist_id", Integer.toString((int)artistRowId)); 1501 values.put("album_id", Integer.toString((int)albumRowId)); 1502 so = values.getAsString("title"); 1503 s = (so == null ? "" : so.toString()); 1504 values.put("title_key", MediaStore.Audio.keyFor(s)); 1505 // do a final trim of the title, in case it started with the special 1506 // "sort first" character (ascii \001) 1507 values.remove("title"); 1508 values.put("title", s.trim()); 1509 1510 computeDisplayName(values.getAsString("_data"), values); 1511 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1512 1513 rowId = db.insert("audio_meta", "duration", values); 1514 if (rowId > 0) { 1515 newUri = ContentUris.withAppendedId(Audio.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 1516 } 1517 break; 1518 } 1519 1520 case AUDIO_MEDIA_ID_GENRES: { 1521 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 1522 ContentValues values = new ContentValues(initialValues); 1523 values.put(Audio.Genres.Members.AUDIO_ID, audioId); 1524 rowId = db.insert("audio_playlists_map", "genre_id", values); 1525 if (rowId > 0) { 1526 newUri = ContentUris.withAppendedId(uri, rowId); 1527 } 1528 break; 1529 } 1530 1531 case AUDIO_MEDIA_ID_PLAYLISTS: { 1532 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 1533 ContentValues values = new ContentValues(initialValues); 1534 values.put(Audio.Playlists.Members.AUDIO_ID, audioId); 1535 rowId = db.insert("audio_playlists_map", "playlist_id", 1536 values); 1537 if (rowId > 0) { 1538 newUri = ContentUris.withAppendedId(uri, rowId); 1539 } 1540 break; 1541 } 1542 1543 case AUDIO_GENRES: { 1544 rowId = db.insert("audio_genres", "audio_id", initialValues); 1545 if (rowId > 0) { 1546 newUri = ContentUris.withAppendedId(Audio.Genres.getContentUri(uri.getPathSegments().get(0)), rowId); 1547 } 1548 break; 1549 } 1550 1551 case AUDIO_GENRES_ID_MEMBERS: { 1552 Long genreId = Long.parseLong(uri.getPathSegments().get(3)); 1553 ContentValues values = new ContentValues(initialValues); 1554 values.put(Audio.Genres.Members.GENRE_ID, genreId); 1555 rowId = db.insert("audio_genres_map", "genre_id", values); 1556 if (rowId > 0) { 1557 newUri = ContentUris.withAppendedId(uri, rowId); 1558 } 1559 break; 1560 } 1561 1562 case AUDIO_PLAYLISTS: { 1563 ContentValues values = new ContentValues(initialValues); 1564 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 1565 rowId = db.insert("audio_playlists", "name", initialValues); 1566 if (rowId > 0) { 1567 newUri = ContentUris.withAppendedId(Audio.Playlists.getContentUri(uri.getPathSegments().get(0)), rowId); 1568 } 1569 break; 1570 } 1571 1572 case AUDIO_PLAYLISTS_ID: 1573 case AUDIO_PLAYLISTS_ID_MEMBERS: { 1574 Long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 1575 ContentValues values = new ContentValues(initialValues); 1576 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId); 1577 rowId = db.insert("audio_playlists_map", "playlist_id", 1578 values); 1579 if (rowId > 0) { 1580 newUri = ContentUris.withAppendedId(uri, rowId); 1581 } 1582 break; 1583 } 1584 1585 case VIDEO_MEDIA: { 1586 ContentValues values = ensureFile(database.mInternal, initialValues, ".3gp", "video"); 1587 String data = values.getAsString("_data"); 1588 computeDisplayName(data, values); 1589 computeBucketValues(data, values); 1590 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1591 rowId = db.insert("video", "artist", values); 1592 if (rowId > 0) { 1593 newUri = ContentUris.withAppendedId(Video.Media.getContentUri( 1594 uri.getPathSegments().get(0)), rowId); 1595 requestMediaThumbnail(data, newUri, 0); 1596 } 1597 break; 1598 } 1599 1600 case AUDIO_ALBUMART: 1601 if (database.mInternal) { 1602 throw new UnsupportedOperationException("no internal album art allowed"); 1603 } 1604 ContentValues values = null; 1605 try { 1606 values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 1607 } catch (IllegalStateException ex) { 1608 // probably no more room to store albumthumbs 1609 values = initialValues; 1610 } 1611 rowId = db.insert("album_art", "_data", values); 1612 if (rowId > 0) { 1613 newUri = ContentUris.withAppendedId(uri, rowId); 1614 } 1615 break; 1616 1617 case VOLUMES: 1618 return attachVolume(initialValues.getAsString("name")); 1619 1620 default: 1621 throw new UnsupportedOperationException("Invalid URI " + uri); 1622 } 1623 1624 return newUri; 1625 } 1626 1627 private MediaThumbRequest requestMediaThumbnail(String path, Uri uri, int priority) { 1628 synchronized (mMediaThumbQueue) { 1629 // Log.v(TAG, "requestMediaThumbnail: "+path+", "+uri+", priority="+priority); 1630 MediaThumbRequest req = new MediaThumbRequest( 1631 getContext().getContentResolver(), path, uri, priority); 1632 mMediaThumbQueue.add(req); 1633 // Trigger the handler. 1634 Message msg = mThumbHandler.obtainMessage(IMAGE_THUMB); 1635 msg.sendToTarget(); 1636 return req; 1637 } 1638 } 1639 1640 private String generateFileName(boolean internal, String preferredExtension, String directoryName) 1641 { 1642 // create a random file 1643 String name = String.valueOf(System.currentTimeMillis()); 1644 1645 if (internal) { 1646 throw new UnsupportedOperationException("Writing to internal storage is not supported."); 1647// return Environment.getDataDirectory() 1648// + "/" + directoryName + "/" + name + preferredExtension; 1649 } else { 1650 return Environment.getExternalStorageDirectory() 1651 + "/" + directoryName + "/" + name + preferredExtension; 1652 } 1653 } 1654 1655 private boolean ensureFileExists(String path) { 1656 File file = new File(path); 1657 if (file.exists()) { 1658 return true; 1659 } else { 1660 // we will not attempt to create the first directory in the path 1661 // (for example, do not create /sdcard if the SD card is not mounted) 1662 int secondSlash = path.indexOf('/', 1); 1663 if (secondSlash < 1) return false; 1664 String directoryPath = path.substring(0, secondSlash); 1665 File directory = new File(directoryPath); 1666 if (!directory.exists()) 1667 return false; 1668 file.getParentFile().mkdirs(); 1669 try { 1670 return file.createNewFile(); 1671 } catch(IOException ioe) { 1672 Log.e(TAG, "File creation failed", ioe); 1673 } 1674 return false; 1675 } 1676 } 1677 1678 private static final class GetTableAndWhereOutParameter { 1679 public String table; 1680 public String where; 1681 } 1682 1683 static final GetTableAndWhereOutParameter sGetTableAndWhereParam = 1684 new GetTableAndWhereOutParameter(); 1685 1686 private void getTableAndWhere(Uri uri, int match, String userWhere, 1687 GetTableAndWhereOutParameter out) { 1688 String where = null; 1689 switch (match) { 1690 case IMAGES_MEDIA: 1691 out.table = "images"; 1692 break; 1693 1694 case IMAGES_MEDIA_ID: 1695 out.table = "images"; 1696 where = "_id = " + uri.getPathSegments().get(3); 1697 break; 1698 1699 case IMAGES_THUMBNAILS_ID: 1700 where = "_id=" + uri.getPathSegments().get(3); 1701 case IMAGES_THUMBNAILS: 1702 out.table = "thumbnails"; 1703 break; 1704 1705 case AUDIO_MEDIA: 1706 out.table = "audio"; 1707 break; 1708 1709 case AUDIO_MEDIA_ID: 1710 out.table = "audio"; 1711 where = "_id=" + uri.getPathSegments().get(3); 1712 break; 1713 1714 case AUDIO_MEDIA_ID_GENRES: 1715 out.table = "audio_genres"; 1716 where = "audio_id=" + uri.getPathSegments().get(3); 1717 break; 1718 1719 case AUDIO_MEDIA_ID_GENRES_ID: 1720 out.table = "audio_genres"; 1721 where = "audio_id=" + uri.getPathSegments().get(3) + 1722 " AND genre_id=" + uri.getPathSegments().get(5); 1723 break; 1724 1725 case AUDIO_MEDIA_ID_PLAYLISTS: 1726 out.table = "audio_playlists"; 1727 where = "audio_id=" + uri.getPathSegments().get(3); 1728 break; 1729 1730 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1731 out.table = "audio_playlists"; 1732 where = "audio_id=" + uri.getPathSegments().get(3) + 1733 " AND playlists_id=" + uri.getPathSegments().get(5); 1734 break; 1735 1736 case AUDIO_GENRES: 1737 out.table = "audio_genres"; 1738 break; 1739 1740 case AUDIO_GENRES_ID: 1741 out.table = "audio_genres"; 1742 where = "_id=" + uri.getPathSegments().get(3); 1743 break; 1744 1745 case AUDIO_GENRES_ID_MEMBERS: 1746 out.table = "audio_genres"; 1747 where = "genre_id=" + uri.getPathSegments().get(3); 1748 break; 1749 1750 case AUDIO_GENRES_ID_MEMBERS_ID: 1751 out.table = "audio_genres"; 1752 where = "genre_id=" + uri.getPathSegments().get(3) + 1753 " AND audio_id =" + uri.getPathSegments().get(5); 1754 break; 1755 1756 case AUDIO_PLAYLISTS: 1757 out.table = "audio_playlists"; 1758 break; 1759 1760 case AUDIO_PLAYLISTS_ID: 1761 out.table = "audio_playlists"; 1762 where = "_id=" + uri.getPathSegments().get(3); 1763 break; 1764 1765 case AUDIO_PLAYLISTS_ID_MEMBERS: 1766 out.table = "audio_playlists_map"; 1767 where = "playlist_id=" + uri.getPathSegments().get(3); 1768 break; 1769 1770 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1771 out.table = "audio_playlists_map"; 1772 where = "playlist_id=" + uri.getPathSegments().get(3) + 1773 " AND _id=" + uri.getPathSegments().get(5); 1774 break; 1775 1776 case AUDIO_ALBUMART_ID: 1777 out.table = "album_art"; 1778 where = "album_id=" + uri.getPathSegments().get(3); 1779 break; 1780 1781 case VIDEO_MEDIA: 1782 out.table = "video"; 1783 break; 1784 1785 case VIDEO_MEDIA_ID: 1786 out.table = "video"; 1787 where = "_id=" + uri.getPathSegments().get(3); 1788 break; 1789 1790 case VIDEO_THUMBNAILS_ID: 1791 where = "_id=" + uri.getPathSegments().get(3); 1792 case VIDEO_THUMBNAILS: 1793 out.table = "videothumbnails"; 1794 break; 1795 1796 default: 1797 throw new UnsupportedOperationException( 1798 "Unknown or unsupported URL: " + uri.toString()); 1799 } 1800 1801 // Add in the user requested WHERE clause, if needed 1802 if (!TextUtils.isEmpty(userWhere)) { 1803 if (!TextUtils.isEmpty(where)) { 1804 out.where = where + " AND (" + userWhere + ")"; 1805 } else { 1806 out.where = userWhere; 1807 } 1808 } else { 1809 out.where = where; 1810 } 1811 } 1812 1813 @Override 1814 public int delete(Uri uri, String userWhere, String[] whereArgs) { 1815 int count; 1816 int match = URI_MATCHER.match(uri); 1817 1818 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1819 if (match == MEDIA_SCANNER) { 1820 if (mMediaScannerVolume == null) { 1821 return 0; 1822 } 1823 mMediaScannerVolume = null; 1824 return 1; 1825 } 1826 1827 if (match != VOLUMES_ID) { 1828 DatabaseHelper database = getDatabaseForUri(uri); 1829 if (database == null) { 1830 throw new UnsupportedOperationException( 1831 "Unknown URI: " + uri); 1832 } 1833 SQLiteDatabase db = database.getWritableDatabase(); 1834 1835 synchronized (sGetTableAndWhereParam) { 1836 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 1837 switch (match) { 1838 case AUDIO_MEDIA: 1839 case AUDIO_MEDIA_ID: 1840 count = db.delete("audio_meta", 1841 sGetTableAndWhereParam.where, whereArgs); 1842 break; 1843 default: 1844 count = db.delete(sGetTableAndWhereParam.table, 1845 sGetTableAndWhereParam.where, whereArgs); 1846 break; 1847 } 1848 getContext().getContentResolver().notifyChange(uri, null); 1849 } 1850 } else { 1851 detachVolume(uri); 1852 count = 1; 1853 } 1854 1855 return count; 1856 } 1857 1858 @Override 1859 public int update(Uri uri, ContentValues initialValues, String userWhere, 1860 String[] whereArgs) { 1861 int count; 1862 // Log.v(TAG, "update for uri="+uri+", initValues="+initialValues); 1863 int match = URI_MATCHER.match(uri); 1864 DatabaseHelper database = getDatabaseForUri(uri); 1865 if (database == null) { 1866 throw new UnsupportedOperationException( 1867 "Unknown URI: " + uri); 1868 } 1869 SQLiteDatabase db = database.getWritableDatabase(); 1870 1871 synchronized (sGetTableAndWhereParam) { 1872 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 1873 1874 switch (match) { 1875 case AUDIO_MEDIA: 1876 case AUDIO_MEDIA_ID: 1877 { 1878 ContentValues values = new ContentValues(initialValues); 1879 // Insert the artist into the artist table and remove it from 1880 // the input values 1881 String artist = values.getAsString("artist"); 1882 if (artist != null) { 1883 values.remove("artist"); 1884 long artistRowId; 1885 HashMap<String, Long> artistCache = database.mArtistCache; 1886 synchronized(artistCache) { 1887 Long temp = artistCache.get(artist); 1888 if (temp == null) { 1889 artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist", 1890 artist, artist, null, 0, null, artistCache, uri); 1891 } else { 1892 artistRowId = temp.longValue(); 1893 } 1894 } 1895 values.put("artist_id", Integer.toString((int)artistRowId)); 1896 } 1897 1898 // Do the same for the album field. 1899 String so = values.getAsString("album"); 1900 if (so != null) { 1901 String path = values.getAsString("_data"); 1902 int albumHash = 0; 1903 if (path == null) { 1904 // If the path is null, we don't have a hash for the file in question. 1905 Log.w(TAG, "Update without specified path."); 1906 } else { 1907 albumHash = path.substring(0, path.lastIndexOf('/')).hashCode(); 1908 } 1909 String s = so.toString(); 1910 values.remove("album"); 1911 long albumRowId; 1912 HashMap<String, Long> albumCache = database.mAlbumCache; 1913 synchronized(albumCache) { 1914 String cacheName = s + albumHash; 1915 Long temp = albumCache.get(cacheName); 1916 if (temp == null) { 1917 albumRowId = getKeyIdForName(db, "albums", "album_key", "album", 1918 s, cacheName, path, albumHash, artist, albumCache, uri); 1919 } else { 1920 albumRowId = temp.longValue(); 1921 } 1922 } 1923 values.put("album_id", Integer.toString((int)albumRowId)); 1924 } 1925 1926 // don't allow the title_key field to be updated directly 1927 values.remove("title_key"); 1928 // If the title field is modified, update the title_key 1929 so = values.getAsString("title"); 1930 if (so != null) { 1931 String s = so.toString(); 1932 values.put("title_key", MediaStore.Audio.keyFor(s)); 1933 // do a final trim of the title, in case it started with the special 1934 // "sort first" character (ascii \001) 1935 values.remove("title"); 1936 values.put("title", s.trim()); 1937 } 1938 1939 count = db.update("audio_meta", values, sGetTableAndWhereParam.where, 1940 whereArgs); 1941 } 1942 break; 1943 case IMAGES_MEDIA: 1944 case IMAGES_MEDIA_ID: 1945 case VIDEO_MEDIA: 1946 case VIDEO_MEDIA_ID: 1947 { 1948 ContentValues values = new ContentValues(initialValues); 1949 // Don't allow bucket id or display name to be updated directly. 1950 // The same names are used for both images and table columns, so 1951 // we use the ImageColumns constants here. 1952 values.remove(ImageColumns.BUCKET_ID); 1953 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 1954 // If the data is being modified update the bucket values 1955 String data = values.getAsString(MediaColumns.DATA); 1956 if (data != null) { 1957 computeBucketValues(data, values); 1958 } 1959 count = db.update(sGetTableAndWhereParam.table, values, 1960 sGetTableAndWhereParam.where, whereArgs); 1961 // if this is a request from MediaScanner, DATA should contains file path 1962 // we only process update request from media scanner, otherwise the requests 1963 // could be duplicate. 1964 if (count > 0 && values.getAsString(MediaStore.MediaColumns.DATA) != null) { 1965 Cursor c = db.query(sGetTableAndWhereParam.table, 1966 READY_FLAG_PROJECTION, sGetTableAndWhereParam.where, 1967 whereArgs, null, null, null); 1968 if (c != null) { 1969 while (c.moveToNext()) { 1970 long magic = c.getLong(2); 1971 if (magic == 0) { 1972 requestMediaThumbnail(c.getString(1), uri, 1973 MediaThumbRequest.PRIORITY_NORMAL); 1974 } 1975 } 1976 c.close(); 1977 } 1978 } 1979 } 1980 break; 1981 default: 1982 count = db.update(sGetTableAndWhereParam.table, initialValues, 1983 sGetTableAndWhereParam.where, whereArgs); 1984 break; 1985 } 1986 } 1987 if (count > 0) { 1988 getContext().getContentResolver().notifyChange(uri, null); 1989 } 1990 return count; 1991 } 1992 1993 private static final String[] openFileColumns = new String[] { 1994 MediaStore.MediaColumns.DATA, 1995 }; 1996 1997 @Override 1998 public ParcelFileDescriptor openFile(Uri uri, String mode) 1999 throws FileNotFoundException { 2000 2001 ParcelFileDescriptor pfd = null; 2002 2003 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_FILE_ID) { 2004 // get album art for the specified media file 2005 DatabaseHelper database = getDatabaseForUri(uri); 2006 if (database == null) { 2007 throw new IllegalStateException("Couldn't open database for " + uri); 2008 } 2009 SQLiteDatabase db = database.getReadableDatabase(); 2010 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2011 int songid = Integer.parseInt(uri.getPathSegments().get(3)); 2012 qb.setTables("audio_meta"); 2013 qb.appendWhere("_id=" + songid); 2014 Cursor c = qb.query(db, 2015 new String [] { 2016 MediaStore.Audio.Media.DATA, 2017 MediaStore.Audio.Media.ALBUM_ID }, 2018 null, null, null, null, null); 2019 if (c.moveToFirst()) { 2020 String audiopath = c.getString(0); 2021 int albumid = c.getInt(1); 2022 // Try to get existing album art for this album first, which 2023 // could possibly have been obtained from a different file. 2024 // If that fails, try to get it from this specific file. 2025 Uri newUri = ContentUris.withAppendedId(ALBUMART_URI, albumid); 2026 try { 2027 pfd = openFile(newUri, mode); // recursive call 2028 } catch (FileNotFoundException ex) { 2029 // That didn't work, now try to get it from the specific file 2030 pfd = getThumb(db, audiopath, albumid, null); 2031 } 2032 } 2033 c.close(); 2034 return pfd; 2035 } 2036 2037 try { 2038 pfd = openFileHelper(uri, mode); 2039 } catch (FileNotFoundException ex) { 2040 if (mode.contains("w")) { 2041 // if the file couldn't be created, we shouldn't extract album art 2042 throw ex; 2043 } 2044 2045 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_ID) { 2046 // Tried to open an album art file which does not exist. Regenerate. 2047 DatabaseHelper database = getDatabaseForUri(uri); 2048 if (database == null) { 2049 throw ex; 2050 } 2051 SQLiteDatabase db = database.getReadableDatabase(); 2052 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2053 int albumid = Integer.parseInt(uri.getPathSegments().get(3)); 2054 qb.setTables("audio_meta"); 2055 qb.appendWhere("album_id=" + albumid); 2056 Cursor c = qb.query(db, 2057 new String [] { 2058 MediaStore.Audio.Media.DATA }, 2059 null, null, null, null, null); 2060 if (c.moveToFirst()) { 2061 String audiopath = c.getString(0); 2062 pfd = getThumb(db, audiopath, albumid, uri); 2063 } 2064 c.close(); 2065 } 2066 if (pfd == null) { 2067 throw ex; 2068 } 2069 } 2070 return pfd; 2071 } 2072 2073 private class Worker implements Runnable { 2074 private final Object mLock = new Object(); 2075 private Looper mLooper; 2076 2077 Worker(String name) { 2078 Thread t = new Thread(null, this, name); 2079 t.start(); 2080 synchronized (mLock) { 2081 while (mLooper == null) { 2082 try { 2083 mLock.wait(); 2084 } catch (InterruptedException ex) { 2085 } 2086 } 2087 } 2088 } 2089 2090 public Looper getLooper() { 2091 return mLooper; 2092 } 2093 2094 public void run() { 2095 synchronized (mLock) { 2096 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 2097 Looper.prepare(); 2098 mLooper = Looper.myLooper(); 2099 mLock.notifyAll(); 2100 } 2101 Looper.loop(); 2102 } 2103 2104 public void quit() { 2105 mLooper.quit(); 2106 } 2107 } 2108 2109 private class ThumbData { 2110 SQLiteDatabase db; 2111 String path; 2112 long album_id; 2113 Uri albumart_uri; 2114 } 2115 2116 private void makeThumbAsync(SQLiteDatabase db, String path, long album_id) { 2117 synchronized (mPendingThumbs) { 2118 if (mPendingThumbs.contains(path)) { 2119 // There's already a request to make an album art thumbnail 2120 // for this audio file in the queue. 2121 return; 2122 } 2123 2124 mPendingThumbs.add(path); 2125 } 2126 2127 ThumbData d = new ThumbData(); 2128 d.db = db; 2129 d.path = path; 2130 d.album_id = album_id; 2131 d.albumart_uri = ContentUris.withAppendedId(mAlbumArtBaseUri, album_id); 2132 2133 // Instead of processing thumbnail requests in the order they were 2134 // received we instead process them stack-based, i.e. LIFO. 2135 // The idea behind this is that the most recently requested thumbnails 2136 // are most likely the ones still in the user's view, whereas those 2137 // requested earlier may have already scrolled off. 2138 synchronized (mThumbRequestStack) { 2139 mThumbRequestStack.push(d); 2140 } 2141 2142 // Trigger the handler. 2143 Message msg = mThumbHandler.obtainMessage(ALBUM_THUMB); 2144 msg.sendToTarget(); 2145 } 2146 2147 // Extract compressed image data from the audio file itself or, if that fails, 2148 // look for a file "AlbumArt.jpg" in the containing directory. 2149 private static byte[] getCompressedAlbumArt(Context context, String path) { 2150 byte[] compressed = null; 2151 2152 try { 2153 File f = new File(path); 2154 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, 2155 ParcelFileDescriptor.MODE_READ_ONLY); 2156 2157 MediaScanner scanner = new MediaScanner(context); 2158 compressed = scanner.extractAlbumArt(pfd.getFileDescriptor()); 2159 pfd.close(); 2160 2161 // If no embedded art exists, look for a suitable image file in the 2162 // same directory as the media file. 2163 // We look for, in order of preference: 2164 // 0 AlbumArt.jpg 2165 // 1 AlbumArt*Large.jpg 2166 // 2 Any other jpg image with 'albumart' anywhere in the name 2167 // 3 Any other jpg image 2168 // 4 any other png image 2169 if (compressed == null && path != null) { 2170 int lastSlash = path.lastIndexOf('/'); 2171 if (lastSlash > 0) { 2172 2173 String artPath = path.substring(0, lastSlash + 1); 2174 2175 String bestmatch = null; 2176 synchronized (sFolderArtMap) { 2177 if (sFolderArtMap.containsKey(artPath)) { 2178 bestmatch = sFolderArtMap.get(artPath); 2179 } else { 2180 File dir = new File(artPath); 2181 String [] entrynames = dir.list(); 2182 if (entrynames == null) { 2183 return null; 2184 } 2185 bestmatch = null; 2186 int matchlevel = 1000; 2187 for (int i = entrynames.length - 1; i >=0; i--) { 2188 String entry = entrynames[i].toLowerCase(); 2189 if (entry.equals("albumart.jpg")) { 2190 bestmatch = entrynames[i]; 2191 break; 2192 } else if (entry.startsWith("albumart") 2193 && entry.endsWith("large.jpg") 2194 && matchlevel > 1) { 2195 bestmatch = entrynames[i]; 2196 matchlevel = 1; 2197 } else if (entry.contains("albumart") 2198 && entry.endsWith(".jpg") 2199 && matchlevel > 2) { 2200 bestmatch = entrynames[i]; 2201 matchlevel = 2; 2202 } else if (entry.endsWith(".jpg") && matchlevel > 3) { 2203 bestmatch = entrynames[i]; 2204 matchlevel = 3; 2205 } else if (entry.endsWith(".png") && matchlevel > 4) { 2206 bestmatch = entrynames[i]; 2207 matchlevel = 4; 2208 } 2209 } 2210 // note that this may insert null if no album art was found 2211 sFolderArtMap.put(artPath, bestmatch); 2212 } 2213 } 2214 2215 if (bestmatch != null) { 2216 File file = new File(artPath + bestmatch); 2217 if (file.exists()) { 2218 compressed = new byte[(int)file.length()]; 2219 FileInputStream stream = null; 2220 try { 2221 stream = new FileInputStream(file); 2222 stream.read(compressed); 2223 } catch (IOException ex) { 2224 compressed = null; 2225 } finally { 2226 if (stream != null) { 2227 stream.close(); 2228 } 2229 } 2230 } 2231 } 2232 } 2233 } 2234 } catch (IOException e) { 2235 } 2236 2237 return compressed; 2238 } 2239 2240 // Return a URI to write the album art to and update the database as necessary. 2241 Uri getAlbumArtOutputUri(SQLiteDatabase db, long album_id, Uri albumart_uri) { 2242 Uri out = null; 2243 // TODO: this could be done more efficiently with a call to db.replace(), which 2244 // replaces or inserts as needed, making it unnecessary to query() first. 2245 if (albumart_uri != null) { 2246 Cursor c = query(albumart_uri, new String [] { "_data" }, 2247 null, null, null); 2248 if (c.moveToFirst()) { 2249 String albumart_path = c.getString(0); 2250 if (ensureFileExists(albumart_path)) { 2251 out = albumart_uri; 2252 } 2253 } else { 2254 albumart_uri = null; 2255 } 2256 c.close(); 2257 } 2258 if (albumart_uri == null){ 2259 ContentValues initialValues = new ContentValues(); 2260 initialValues.put("album_id", album_id); 2261 try { 2262 ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 2263 long rowId = db.insert("album_art", "_data", values); 2264 if (rowId > 0) { 2265 out = ContentUris.withAppendedId(ALBUMART_URI, rowId); 2266 } 2267 } catch (IllegalStateException ex) { 2268 Log.e(TAG, "error creating album thumb file"); 2269 } 2270 } 2271 return out; 2272 } 2273 2274 // Write out the album art to the output URI, recompresses the given Bitmap 2275 // if necessary, otherwise writes the compressed data. 2276 private void writeAlbumArt( 2277 boolean need_to_recompress, Uri out, byte[] compressed, Bitmap bm) { 2278 boolean success = false; 2279 try { 2280 OutputStream outstream = getContext().getContentResolver().openOutputStream(out); 2281 2282 if (!need_to_recompress) { 2283 // No need to recompress here, just write out the original 2284 // compressed data here. 2285 outstream.write(compressed); 2286 success = true; 2287 } else { 2288 success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream); 2289 } 2290 2291 outstream.close(); 2292 } catch (FileNotFoundException ex) { 2293 Log.e(TAG, "error creating file", ex); 2294 } catch (IOException ex) { 2295 Log.e(TAG, "error creating file", ex); 2296 } 2297 if (!success) { 2298 // the thumbnail was not written successfully, delete the entry that refers to it 2299 getContext().getContentResolver().delete(out, null, null); 2300 } 2301 } 2302 2303 private ParcelFileDescriptor getThumb(SQLiteDatabase db, String path, long album_id, 2304 Uri albumart_uri) { 2305 ThumbData d = new ThumbData(); 2306 d.db = db; 2307 d.path = path; 2308 d.album_id = album_id; 2309 d.albumart_uri = albumart_uri; 2310 return makeThumbInternal(d); 2311 } 2312 2313 private ParcelFileDescriptor makeThumbInternal(ThumbData d) { 2314 byte[] compressed = getCompressedAlbumArt(getContext(), d.path); 2315 2316 if (compressed == null) { 2317 return null; 2318 } 2319 2320 Bitmap bm = null; 2321 boolean need_to_recompress = true; 2322 2323 try { 2324 // get the size of the bitmap 2325 BitmapFactory.Options opts = new BitmapFactory.Options(); 2326 opts.inJustDecodeBounds = true; 2327 opts.inSampleSize = 1; 2328 BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts); 2329 2330 // request a reasonably sized output image 2331 // TODO: don't hardcode the size 2332 while (opts.outHeight > 320 || opts.outWidth > 320) { 2333 opts.outHeight /= 2; 2334 opts.outWidth /= 2; 2335 opts.inSampleSize *= 2; 2336 } 2337 2338 if (opts.inSampleSize == 1) { 2339 // The original album art was of proper size, we won't have to 2340 // recompress the bitmap later. 2341 need_to_recompress = false; 2342 } else { 2343 // get the image for real now 2344 opts.inJustDecodeBounds = false; 2345 opts.inPreferredConfig = Bitmap.Config.RGB_565; 2346 bm = BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts); 2347 2348 if (bm != null && bm.getConfig() == null) { 2349 bm = bm.copy(Bitmap.Config.RGB_565, false); 2350 } 2351 } 2352 } catch (Exception e) { 2353 } 2354 2355 if (need_to_recompress && bm == null) { 2356 return null; 2357 } 2358 2359 if (d.albumart_uri == null) { 2360 // this one doesn't need to be saved (probably a song with an unknown album), 2361 // so stick it in a memory file and return that 2362 try { 2363 MemoryFile file = new MemoryFile("albumthumb", compressed.length); 2364 file.writeBytes(compressed, 0, 0, compressed.length); 2365 file.deactivate(); 2366 return file.getParcelFileDescriptor(); 2367 } catch (IOException e) { 2368 } 2369 } else { 2370 // this one needs to actually be saved on the sd card 2371 Uri out = getAlbumArtOutputUri(d.db, d.album_id, d.albumart_uri); 2372 2373 if (out != null) { 2374 writeAlbumArt(need_to_recompress, out, compressed, bm); 2375 getContext().getContentResolver().notifyChange(MEDIA_URI, null); 2376 try { 2377 return openFileHelper(out, "r"); 2378 } catch (FileNotFoundException ex) { 2379 } 2380 } 2381 } 2382 return null; 2383 } 2384 2385 /** 2386 * Look up the artist or album entry for the given name, creating that entry 2387 * if it does not already exists. 2388 * @param db The database 2389 * @param table The table to store the key/name pair in. 2390 * @param keyField The name of the key-column 2391 * @param nameField The name of the name-column 2392 * @param rawName The name that the calling app was trying to insert into the database 2393 * @param cacheName The string that will be inserted in to the cache 2394 * @param path The full path to the file being inserted in to the audio table 2395 * @param albumHash A hash to distinguish between different albums of the same name 2396 * @param artist The name of the artist, if known 2397 * @param cache The cache to add this entry to 2398 * @param srcuri The Uri that prompted the call to this method, used for determining whether this is 2399 * the internal or external database 2400 * @return The row ID for this artist/album, or -1 if the provided name was invalid 2401 */ 2402 private long getKeyIdForName(SQLiteDatabase db, String table, String keyField, String nameField, 2403 String rawName, String cacheName, String path, int albumHash, 2404 String artist, HashMap<String, Long> cache, Uri srcuri) { 2405 long rowId; 2406 2407 if (rawName == null || rawName.length() == 0) { 2408 return -1; 2409 } 2410 String k = MediaStore.Audio.keyFor(rawName); 2411 2412 if (k == null) { 2413 return -1; 2414 } 2415 2416 boolean isAlbum = table.equals("albums"); 2417 boolean isUnknown = MediaFile.UNKNOWN_STRING.equals(rawName); 2418 2419 // To distinguish same-named albums, we append a hash of the path. 2420 // Ideally we would also take things like CDDB ID in to account, so 2421 // we can group files from the same album that aren't in the same 2422 // folder, but this is a quick and easy start that works immediately 2423 // without requiring support from the mp3, mp4 and Ogg meta data 2424 // readers, as long as the albums are in different folders. 2425 if (isAlbum) { 2426 k = k + albumHash; 2427 if (isUnknown) { 2428 k = k + artist; 2429 } 2430 } 2431 2432 String [] selargs = { k }; 2433 Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null); 2434 2435 try { 2436 switch (c.getCount()) { 2437 case 0: { 2438 // insert new entry into table 2439 ContentValues otherValues = new ContentValues(); 2440 otherValues.put(keyField, k); 2441 otherValues.put(nameField, rawName); 2442 rowId = db.insert(table, "duration", otherValues); 2443 if (path != null && isAlbum && ! isUnknown) { 2444 // We just inserted a new album. Now create an album art thumbnail for it. 2445 makeThumbAsync(db, path, rowId); 2446 } 2447 if (rowId > 0) { 2448 String volume = srcuri.toString().substring(16, 24); // extract internal/external 2449 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 2450 getContext().getContentResolver().notifyChange(uri, null); 2451 } 2452 } 2453 break; 2454 case 1: { 2455 // Use the existing entry 2456 c.moveToFirst(); 2457 rowId = c.getLong(0); 2458 2459 // Determine whether the current rawName is better than what's 2460 // currently stored in the table, and update the table if it is. 2461 String currentFancyName = c.getString(2); 2462 String bestName = makeBestName(rawName, currentFancyName); 2463 if (!bestName.equals(currentFancyName)) { 2464 // update the table with the new name 2465 ContentValues newValues = new ContentValues(); 2466 newValues.put(nameField, bestName); 2467 db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null); 2468 String volume = srcuri.toString().substring(16, 24); // extract internal/external 2469 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 2470 getContext().getContentResolver().notifyChange(uri, null); 2471 } 2472 } 2473 break; 2474 default: 2475 // corrupt database 2476 Log.e(TAG, "Multiple entries in table " + table + " for key " + k); 2477 rowId = -1; 2478 break; 2479 } 2480 } finally { 2481 if (c != null) c.close(); 2482 } 2483 2484 if (cache != null && ! isUnknown) { 2485 cache.put(cacheName, rowId); 2486 } 2487 return rowId; 2488 } 2489 2490 /** 2491 * Returns the best string to use for display, given two names. 2492 * Note that this function does not necessarily return either one 2493 * of the provided names; it may decide to return a better alternative 2494 * (for example, specifying the inputs "Police" and "Police, The" will 2495 * return "The Police") 2496 * 2497 * The basic assumptions are: 2498 * - longer is better ("The police" is better than "Police") 2499 * - prefix is better ("The Police" is better than "Police, The") 2500 * - accents are better ("Motörhead" is better than "Motorhead") 2501 * 2502 * @param one The first of the two names to consider 2503 * @param two The last of the two names to consider 2504 * @return The actual name to use 2505 */ 2506 String makeBestName(String one, String two) { 2507 String name; 2508 2509 // Longer names are usually better. 2510 if (one.length() > two.length()) { 2511 name = one; 2512 } else { 2513 // Names with accents are usually better, and conveniently sort later 2514 if (one.toLowerCase().compareTo(two.toLowerCase()) > 0) { 2515 name = one; 2516 } else { 2517 name = two; 2518 } 2519 } 2520 2521 // Prefixes are better than postfixes. 2522 if (name.endsWith(", the") || name.endsWith(",the") || 2523 name.endsWith(", an") || name.endsWith(",an") || 2524 name.endsWith(", a") || name.endsWith(",a")) { 2525 String fix = name.substring(1 + name.lastIndexOf(',')); 2526 name = fix.trim() + " " + name.substring(0, name.lastIndexOf(',')); 2527 } 2528 2529 // TODO: word-capitalize the resulting name 2530 return name; 2531 } 2532 2533 2534 /** 2535 * Looks up the database based on the given URI. 2536 * 2537 * @param uri The requested URI 2538 * @returns the database for the given URI 2539 */ 2540 private DatabaseHelper getDatabaseForUri(Uri uri) { 2541 synchronized (mDatabases) { 2542 if (uri.getPathSegments().size() > 1) { 2543 return mDatabases.get(uri.getPathSegments().get(0)); 2544 } 2545 } 2546 return null; 2547 } 2548 2549 /** 2550 * Attach the database for a volume (internal or external). 2551 * Does nothing if the volume is already attached, otherwise 2552 * checks the volume ID and sets up the corresponding database. 2553 * 2554 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}. 2555 * @return the content URI of the attached volume. 2556 */ 2557 private Uri attachVolume(String volume) { 2558 if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) { 2559 throw new SecurityException( 2560 "Opening and closing databases not allowed."); 2561 } 2562 2563 synchronized (mDatabases) { 2564 if (mDatabases.get(volume) != null) { // Already attached 2565 return Uri.parse("content://media/" + volume); 2566 } 2567 2568 DatabaseHelper db; 2569 if (INTERNAL_VOLUME.equals(volume)) { 2570 db = new DatabaseHelper(getContext(), INTERNAL_DATABASE_NAME, true); 2571 } else if (EXTERNAL_VOLUME.equals(volume)) { 2572 String path = Environment.getExternalStorageDirectory().getPath(); 2573 int volumeID = FileUtils.getFatVolumeId(path); 2574 if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID); 2575 2576 // generate database name based on volume ID 2577 String dbName = "external-" + Integer.toHexString(volumeID) + ".db"; 2578 db = new DatabaseHelper(getContext(), dbName, false); 2579 } else { 2580 throw new IllegalArgumentException("There is no volume named " + volume); 2581 } 2582 2583 mDatabases.put(volume, db); 2584 2585 if (!db.mInternal) { 2586 // clean up stray album art files: delete every file not in the database 2587 File[] files = new File( 2588 Environment.getExternalStorageDirectory(), 2589 ALBUM_THUMB_FOLDER).listFiles(); 2590 HashSet<String> fileSet = new HashSet(); 2591 for (int i = 0; files != null && i < files.length; i++) { 2592 fileSet.add(files[i].getPath()); 2593 } 2594 2595 Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, 2596 new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null); 2597 try { 2598 while (cursor != null && cursor.moveToNext()) { 2599 fileSet.remove(cursor.getString(0)); 2600 } 2601 } finally { 2602 if (cursor != null) cursor.close(); 2603 } 2604 2605 Iterator<String> iterator = fileSet.iterator(); 2606 while (iterator.hasNext()) { 2607 String filename = iterator.next(); 2608 if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename); 2609 new File(filename).delete(); 2610 } 2611 } 2612 } 2613 2614 if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume); 2615 return Uri.parse("content://media/" + volume); 2616 } 2617 2618 /** 2619 * Detach the database for a volume (must be external). 2620 * Does nothing if the volume is already detached, otherwise 2621 * closes the database and sends a notification to listeners. 2622 * 2623 * @param uri The content URI of the volume, as returned by {@link #attachVolume} 2624 */ 2625 private void detachVolume(Uri uri) { 2626 if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) { 2627 throw new SecurityException( 2628 "Opening and closing databases not allowed."); 2629 } 2630 2631 String volume = uri.getPathSegments().get(0); 2632 if (INTERNAL_VOLUME.equals(volume)) { 2633 throw new UnsupportedOperationException( 2634 "Deleting the internal volume is not allowed"); 2635 } else if (!EXTERNAL_VOLUME.equals(volume)) { 2636 throw new IllegalArgumentException( 2637 "There is no volume named " + volume); 2638 } 2639 2640 synchronized (mDatabases) { 2641 DatabaseHelper database = mDatabases.get(volume); 2642 if (database == null) return; 2643 2644 try { 2645 // touch the database file to show it is most recently used 2646 File file = new File(database.getReadableDatabase().getPath()); 2647 file.setLastModified(System.currentTimeMillis()); 2648 } catch (SQLException e) { 2649 Log.e(TAG, "Can't touch database file", e); 2650 } 2651 2652 mDatabases.remove(volume); 2653 database.close(); 2654 } 2655 2656 getContext().getContentResolver().notifyChange(uri, null); 2657 if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume); 2658 } 2659 2660 private static String TAG = "MediaProvider"; 2661 private static final boolean LOCAL_LOGV = false; 2662 private static final int DATABASE_VERSION = 78; 2663 private static final String INTERNAL_DATABASE_NAME = "internal.db"; 2664 2665 // maximum number of cached external databases to keep 2666 private static final int MAX_EXTERNAL_DATABASES = 3; 2667 2668 // Delete databases that have not been used in two months 2669 // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60) 2670 private static final long OBSOLETE_DATABASE_DB = 5184000000L; 2671 2672 private HashMap<String, DatabaseHelper> mDatabases; 2673 2674 private Worker mThumbWorker; 2675 private Handler mThumbHandler; 2676 2677 // name of the volume currently being scanned by the media scanner (or null) 2678 private String mMediaScannerVolume; 2679 2680 static final String INTERNAL_VOLUME = "internal"; 2681 static final String EXTERNAL_VOLUME = "external"; 2682 static final String ALBUM_THUMB_FOLDER = "albumthumbs"; 2683 2684 // path for writing contents of in memory temp database 2685 private String mTempDatabasePath; 2686 2687 private static final int IMAGES_MEDIA = 1; 2688 private static final int IMAGES_MEDIA_ID = 2; 2689 private static final int IMAGES_THUMBNAILS = 3; 2690 private static final int IMAGES_THUMBNAILS_ID = 4; 2691 2692 private static final int AUDIO_MEDIA = 100; 2693 private static final int AUDIO_MEDIA_ID = 101; 2694 private static final int AUDIO_MEDIA_ID_GENRES = 102; 2695 private static final int AUDIO_MEDIA_ID_GENRES_ID = 103; 2696 private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104; 2697 private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105; 2698 private static final int AUDIO_GENRES = 106; 2699 private static final int AUDIO_GENRES_ID = 107; 2700 private static final int AUDIO_GENRES_ID_MEMBERS = 108; 2701 private static final int AUDIO_GENRES_ID_MEMBERS_ID = 109; 2702 private static final int AUDIO_PLAYLISTS = 110; 2703 private static final int AUDIO_PLAYLISTS_ID = 111; 2704 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 2705 private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113; 2706 private static final int AUDIO_ARTISTS = 114; 2707 private static final int AUDIO_ARTISTS_ID = 115; 2708 private static final int AUDIO_ALBUMS = 116; 2709 private static final int AUDIO_ALBUMS_ID = 117; 2710 private static final int AUDIO_ARTISTS_ID_ALBUMS = 118; 2711 private static final int AUDIO_ALBUMART = 119; 2712 private static final int AUDIO_ALBUMART_ID = 120; 2713 private static final int AUDIO_ALBUMART_FILE_ID = 121; 2714 2715 private static final int VIDEO_MEDIA = 200; 2716 private static final int VIDEO_MEDIA_ID = 201; 2717 private static final int VIDEO_THUMBNAILS = 202; 2718 private static final int VIDEO_THUMBNAILS_ID = 203; 2719 2720 private static final int VOLUMES = 300; 2721 private static final int VOLUMES_ID = 301; 2722 2723 private static final int AUDIO_SEARCH_LEGACY = 400; 2724 private static final int AUDIO_SEARCH_BASIC = 401; 2725 private static final int AUDIO_SEARCH_FANCY = 402; 2726 2727 private static final int MEDIA_SCANNER = 500; 2728 2729 private static final UriMatcher URI_MATCHER = 2730 new UriMatcher(UriMatcher.NO_MATCH); 2731 2732 private static final String[] ID_PROJECTION = new String[] { 2733 MediaStore.MediaColumns._ID 2734 }; 2735 2736 private static final String[] MIME_TYPE_PROJECTION = new String[] { 2737 MediaStore.MediaColumns._ID, // 0 2738 MediaStore.MediaColumns.MIME_TYPE, // 1 2739 }; 2740 2741 private static final String[] READY_FLAG_PROJECTION = new String[] { 2742 MediaStore.MediaColumns._ID, 2743 MediaStore.MediaColumns.DATA, 2744 Images.Media.MINI_THUMB_MAGIC 2745 }; 2746 2747 private static final String[] EXTERNAL_DATABASE_TABLES = new String[] { 2748 "images", 2749 "thumbnails", 2750 "audio_meta", 2751 "artists", 2752 "albums", 2753 "audio_genres", 2754 "audio_genres_map", 2755 "audio_playlists", 2756 "audio_playlists_map", 2757 "video", 2758 }; 2759 2760 static 2761 { 2762 URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA); 2763 URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID); 2764 URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS); 2765 URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID); 2766 2767 URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA); 2768 URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID); 2769 URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES); 2770 URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID); 2771 URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS); 2772 URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID); 2773 URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES); 2774 URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID); 2775 URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS); 2776 URI_MATCHER.addURI("media", "*/audio/genres/#/members/#", AUDIO_GENRES_ID_MEMBERS_ID); 2777 URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS); 2778 URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID); 2779 URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS); 2780 URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID); 2781 URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS); 2782 URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID); 2783 URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS); 2784 URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS); 2785 URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID); 2786 URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART); 2787 URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID); 2788 URI_MATCHER.addURI("media", "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID); 2789 2790 URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA); 2791 URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID); 2792 URI_MATCHER.addURI("media", "*/video/thumbnails", VIDEO_THUMBNAILS); 2793 URI_MATCHER.addURI("media", "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID); 2794 2795 URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER); 2796 2797 URI_MATCHER.addURI("media", "*", VOLUMES_ID); 2798 URI_MATCHER.addURI("media", null, VOLUMES); 2799 2800 /** 2801 * @deprecated use the 'basic' or 'fancy' search Uris instead 2802 */ 2803 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY, 2804 AUDIO_SEARCH_LEGACY); 2805 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 2806 AUDIO_SEARCH_LEGACY); 2807 2808 // used for search suggestions 2809 URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY, 2810 AUDIO_SEARCH_BASIC); 2811 URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY + 2812 "/*", AUDIO_SEARCH_BASIC); 2813 2814 // used by the music app's search activity 2815 URI_MATCHER.addURI("media", "*/audio/search/fancy", AUDIO_SEARCH_FANCY); 2816 URI_MATCHER.addURI("media", "*/audio/search/fancy/*", AUDIO_SEARCH_FANCY); 2817 } 2818} 2819