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