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