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