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