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