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