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