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