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