MediaProvider.java revision f414462bdd8715a39e24a7eaee0863e5b29302ce
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.providers.media; 18 19import android.app.SearchManager; 20import android.content.*; 21import android.database.Cursor; 22import android.database.MergeCursor; 23import android.database.SQLException; 24import android.database.sqlite.SQLiteDatabase; 25import android.database.sqlite.SQLiteOpenHelper; 26import android.database.sqlite.SQLiteQueryBuilder; 27import android.graphics.Bitmap; 28import android.graphics.BitmapFactory; 29import android.media.MediaFile; 30import android.media.MediaScanner; 31import android.net.Uri; 32import android.os.Binder; 33import android.os.Environment; 34import android.os.FileUtils; 35import android.os.Handler; 36import android.os.Looper; 37import android.os.Message; 38import android.os.ParcelFileDescriptor; 39import android.os.Process; 40import android.provider.BaseColumns; 41import android.provider.MediaStore; 42import android.provider.MediaStore.Audio; 43import android.provider.MediaStore.Images; 44import android.provider.MediaStore.MediaColumns; 45import android.provider.MediaStore.Video; 46import android.provider.MediaStore.Images.ImageColumns; 47import android.text.TextUtils; 48import android.util.Config; 49import android.util.Log; 50 51import java.io.File; 52import java.io.FileInputStream; 53import java.io.FileNotFoundException; 54import java.io.IOException; 55import java.io.OutputStream; 56import java.text.Collator; 57import java.util.HashMap; 58import java.util.HashSet; 59import java.util.Iterator; 60import java.util.List; 61 62/** 63 * Media content provider. See {@link android.provider.MediaStore} for details. 64 * Separate databases are kept for each external storage card we see (using the 65 * card's ID as an index). The content visible at content://media/external/... 66 * changes with the card. 67 */ 68public class MediaProvider extends ContentProvider { 69 private static final Uri MEDIA_URI = Uri.parse("content://media"); 70 private static final Uri ALBUMART_URI = Uri.parse("content://media/external/audio/albumart"); 71 private static final Uri ALBUMART_THUMB_URI = Uri.parse("content://media/external/audio/albumart_thumb"); 72 73 private static final HashMap<String, String> sArtistAlbumsMap = new HashMap<String, String>(); 74 75 private BroadcastReceiver mUnmountReceiver = new BroadcastReceiver() { 76 @Override 77 public void onReceive(Context context, Intent intent) { 78 if (intent.getAction().equals(Intent.ACTION_MEDIA_EJECT)) { 79 // Remove the external volume and then notify all cursors backed by 80 // data on that volume 81 detachVolume(Uri.parse("content://media/external")); 82 } 83 } 84 }; 85 86 /** 87 * Wrapper class for a specific database (associated with one particular 88 * external card, or with internal storage). Can open the actual database 89 * on demand, create and upgrade the schema, etc. 90 */ 91 private static final class DatabaseHelper extends SQLiteOpenHelper { 92 final Context mContext; 93 final boolean mInternal; // True if this is the internal database 94 95 // In memory caches of artist and album data. 96 HashMap<String, Long> mArtistCache = new HashMap<String, Long>(); 97 HashMap<String, Long> mAlbumCache = new HashMap<String, Long>(); 98 99 public DatabaseHelper(Context context, String name, boolean internal) { 100 super(context, name, null, DATABASE_VERSION); 101 mContext = context; 102 mInternal = internal; 103 } 104 105 /** 106 * Creates database the first time we try to open it. 107 */ 108 @Override 109 public void onCreate(final SQLiteDatabase db) { 110 updateDatabase(db, mInternal, 0, DATABASE_VERSION); 111 } 112 113 /** 114 * Updates the database format when a new content provider is used 115 * with an older database format. 116 */ 117 @Override 118 public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) { 119 updateDatabase(db, mInternal, oldV, newV); 120 } 121 122 /** 123 * Touch this particular database and garbage collect old databases. 124 * An LRU cache system is used to clean up databases for old external 125 * storage volumes. 126 */ 127 @Override 128 public void onOpen(SQLiteDatabase db) { 129 if (mInternal) return; // The internal database is kept separately. 130 131 // touch the database file to show it is most recently used 132 File file = new File(db.getPath()); 133 long now = System.currentTimeMillis(); 134 file.setLastModified(now); 135 136 // delete least recently used databases if we are over the limit 137 String[] databases = mContext.databaseList(); 138 int count = databases.length; 139 int limit = MAX_EXTERNAL_DATABASES; 140 141 // delete external databases that have not been used in the past two months 142 long twoMonthsAgo = now - OBSOLETE_DATABASE_DB; 143 for (int i = 0; i < databases.length; i++) { 144 File other = mContext.getDatabasePath(databases[i]); 145 if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) { 146 databases[i] = null; 147 count--; 148 if (file.equals(other)) { 149 // reduce limit to account for the existence of the database we 150 // are about to open, which we removed from the list. 151 limit--; 152 } 153 } else { 154 long time = other.lastModified(); 155 if (time < twoMonthsAgo) { 156 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[i]); 157 mContext.deleteDatabase(databases[i]); 158 databases[i] = null; 159 count--; 160 } 161 } 162 } 163 164 // delete least recently used databases until 165 // we are no longer over the limit 166 while (count > limit) { 167 int lruIndex = -1; 168 long lruTime = 0; 169 170 for (int i = 0; i < databases.length; i++) { 171 if (databases[i] != null) { 172 long time = mContext.getDatabasePath(databases[i]).lastModified(); 173 if (lruTime == 0 || time < lruTime) { 174 lruIndex = i; 175 lruTime = time; 176 } 177 } 178 } 179 180 // delete least recently used database 181 if (lruIndex != -1) { 182 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]); 183 mContext.deleteDatabase(databases[lruIndex]); 184 databases[lruIndex] = null; 185 count--; 186 } 187 } 188 } 189 } 190 191 @Override 192 public boolean onCreate() { 193 sArtistAlbumsMap.put(MediaStore.Audio.Albums._ID, "audio.album_id AS " + 194 MediaStore.Audio.Albums._ID); 195 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM, "album"); 196 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_KEY, "album_key"); 197 sArtistAlbumsMap.put(MediaStore.Audio.Albums.FIRST_YEAR, "MIN(year) AS " + 198 MediaStore.Audio.Albums.FIRST_YEAR); 199 sArtistAlbumsMap.put(MediaStore.Audio.Albums.LAST_YEAR, "MAX(year) AS " + 200 MediaStore.Audio.Albums.LAST_YEAR); 201 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST, "artist"); 202 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_ID, "artist"); 203 sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_KEY, "artist_key"); 204 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS, "count(*) AS " + 205 MediaStore.Audio.Albums.NUMBER_OF_SONGS); 206 sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_ART, "album_art._data AS " + 207 MediaStore.Audio.Albums.ALBUM_ART); 208 209 mDatabases = new HashMap<String, DatabaseHelper>(); 210 attachVolume(INTERNAL_VOLUME); 211 212 IntentFilter iFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); 213 iFilter.addDataScheme("file"); 214 getContext().registerReceiver(mUnmountReceiver, iFilter); 215 216 // open external database if external storage is mounted 217 String state = Environment.getExternalStorageState(); 218 if (Environment.MEDIA_MOUNTED.equals(state) || 219 Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 220 attachVolume(EXTERNAL_VOLUME); 221 } 222 223 mThumbWorker = new Worker("album thumbs"); 224 mThumbHandler = new Handler(mThumbWorker.getLooper()) { 225 @Override 226 public void handleMessage(Message msg) { 227 makeThumb((ThumbData)msg.obj); 228 } 229 }; 230 231 return true; 232 } 233 234 /** 235 * This method takes care of updating all the tables in the database to the 236 * current version, creating them if necessary. 237 * This method can only update databases at schema 63 or higher, which was 238 * created August 1, 2008. Older database will be cleared and recreated. 239 * @param db Database 240 * @param internal True if this is the internal media database 241 */ 242 private static void updateDatabase(SQLiteDatabase db, boolean internal, 243 int fromVersion, int toVersion) { 244 245 // sanity check 246 if (toVersion != DATABASE_VERSION) { 247 Log.e(TAG, "Illegal update request. Got " + toVersion + ", expected " + 248 DATABASE_VERSION); 249 throw new IllegalArgumentException(); 250 } 251 252 if (fromVersion < 63) { 253 // Drop everything and start over. 254 Log.i(TAG, "Upgrading media database from version " + 255 fromVersion + " to " + toVersion + ", which will destroy all old data"); 256 db.execSQL("DROP TABLE IF EXISTS images"); 257 db.execSQL("DROP TRIGGER IF EXISTS images_cleanup"); 258 db.execSQL("DROP TABLE IF EXISTS thumbnails"); 259 db.execSQL("DROP TRIGGER IF EXISTS thumbnails_cleanup"); 260 db.execSQL("DROP TABLE IF EXISTS audio_meta"); 261 db.execSQL("DROP TABLE IF EXISTS artists"); 262 db.execSQL("DROP TABLE IF EXISTS albums"); 263 db.execSQL("DROP TABLE IF EXISTS album_art"); 264 db.execSQL("DROP VIEW IF EXISTS audio"); 265 db.execSQL("DROP TRIGGER IF EXISTS audio_delete"); 266 db.execSQL("DROP VIEW IF EXISTS artist_info"); 267 db.execSQL("DROP VIEW IF EXISTS album_info"); 268 db.execSQL("DROP VIEW IF EXISTS artists_albums_map"); 269 db.execSQL("DROP TRIGGER IF EXISTS audio_meta_cleanup"); 270 db.execSQL("DROP TABLE IF EXISTS audio_genres"); 271 db.execSQL("DROP TABLE IF EXISTS audio_genres_map"); 272 db.execSQL("DROP TRIGGER IF EXISTS audio_genres_cleanup"); 273 db.execSQL("DROP TABLE IF EXISTS audio_playlists"); 274 db.execSQL("DROP TABLE IF EXISTS audio_playlists_map"); 275 db.execSQL("DROP TRIGGER IF EXISTS audio_playlists_cleanup"); 276 db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup1"); 277 db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup2"); 278 db.execSQL("DROP TABLE IF EXISTS video"); 279 db.execSQL("DROP TRIGGER IF EXISTS video_cleanup"); 280 281 db.execSQL("CREATE TABLE IF NOT EXISTS images (" + 282 "_id INTEGER PRIMARY KEY," + 283 "_data TEXT," + 284 "_size INTEGER," + 285 "_display_name TEXT," + 286 "mime_type TEXT," + 287 "title TEXT," + 288 "date_added INTEGER," + 289 "date_modified INTEGER," + 290 "description TEXT," + 291 "picasa_id TEXT," + 292 "isprivate INTEGER," + 293 "latitude DOUBLE," + 294 "longitude DOUBLE," + 295 "datetaken INTEGER," + 296 "orientation INTEGER," + 297 "mini_thumb_magic INTEGER," + 298 "bucket_id TEXT," + 299 "bucket_display_name TEXT" + 300 ");"); 301 302 db.execSQL("CREATE INDEX IF NOT EXISTS mini_thumb_magic_index on images(mini_thumb_magic);"); 303 304 db.execSQL("CREATE TRIGGER IF NOT EXISTS images_cleanup DELETE ON images " + 305 "BEGIN " + 306 "DELETE FROM thumbnails WHERE image_id = old._id;" + 307 "SELECT _DELETE_FILE(old._data);" + 308 "END"); 309 310 db.execSQL("CREATE TABLE IF NOT EXISTS thumbnails (" + 311 "_id INTEGER PRIMARY KEY," + 312 "_data TEXT," + 313 "image_id INTEGER," + 314 "kind INTEGER," + 315 "width INTEGER," + 316 "height INTEGER" + 317 ");"); 318 319 db.execSQL("CREATE INDEX IF NOT EXISTS image_id_index on thumbnails(image_id);"); 320 321 db.execSQL("CREATE TRIGGER IF NOT EXISTS thumbnails_cleanup DELETE ON thumbnails " + 322 "BEGIN " + 323 "SELECT _DELETE_FILE(old._data);" + 324 "END"); 325 326 327 // Contains meta data about audio files 328 db.execSQL("CREATE TABLE IF NOT EXISTS audio_meta (" + 329 "_id INTEGER PRIMARY KEY," + 330 "_data TEXT NOT NULL," + 331 "_display_name TEXT," + 332 "_size INTEGER," + 333 "mime_type TEXT," + 334 "date_added INTEGER," + 335 "date_modified INTEGER," + 336 "title TEXT NOT NULL," + 337 "title_key TEXT NOT NULL," + 338 "duration INTEGER," + 339 "artist_id INTEGER," + 340 "composer TEXT," + 341 "album_id INTEGER," + 342 "track INTEGER," + // track is an integer to allow proper sorting 343 "year INTEGER CHECK(year!=0)," + 344 "is_ringtone INTEGER," + 345 "is_music INTEGER," + 346 "is_alarm INTEGER," + 347 "is_notification INTEGER" + 348 ");"); 349 350 // Contains a sort/group "key" and the preferred display name for artists 351 db.execSQL("CREATE TABLE IF NOT EXISTS artists (" + 352 "artist_id INTEGER PRIMARY KEY," + 353 "artist_key TEXT NOT NULL UNIQUE," + 354 "artist TEXT NOT NULL" + 355 ");"); 356 357 // Contains a sort/group "key" and the preferred display name for albums 358 db.execSQL("CREATE TABLE IF NOT EXISTS albums (" + 359 "album_id INTEGER PRIMARY KEY," + 360 "album_key TEXT NOT NULL UNIQUE," + 361 "album TEXT NOT NULL" + 362 ");"); 363 364 db.execSQL("CREATE TABLE IF NOT EXISTS album_art (" + 365 "album_id INTEGER PRIMARY KEY," + 366 "_data TEXT" + 367 ");"); 368 369 // Provides a unified audio/artist/album info view. 370 // Note that views are read-only, so we define a trigger to allow deletes. 371 db.execSQL("CREATE VIEW IF NOT EXISTS audio as SELECT * FROM audio_meta " + 372 "LEFT OUTER JOIN artists ON audio_meta.artist_id=artists.artist_id " + 373 "LEFT OUTER JOIN albums ON audio_meta.album_id=albums.album_id;"); 374 375 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_delete INSTEAD OF DELETE ON audio " + 376 "BEGIN " + 377 "DELETE from audio_meta where _id=old._id;" + 378 "DELETE from audio_playlists_map where audio_id=old._id;" + 379 "DELETE from audio_genres_map where audio_id=old._id;" + 380 "END"); 381 382 383 // Provides some extra info about artists, like the number of tracks 384 // and albums for this artist 385 db.execSQL("CREATE VIEW IF NOT EXISTS artist_info AS " + 386 "SELECT artist_id AS _id, artist, artist_key, " + 387 "COUNT(DISTINCT album) AS number_of_albums, " + 388 "COUNT(*) AS number_of_tracks FROM audio WHERE is_music=1 "+ 389 "GROUP BY artist_key;"); 390 391 // Provides extra info albums, such as the number of tracks 392 db.execSQL("CREATE VIEW IF NOT EXISTS album_info AS " + 393 "SELECT audio.album_id AS _id, album, album_key, " + 394 "MIN(year) AS minyear, " + 395 "MAX(year) AS maxyear, artist, artist_id, artist_key, " + 396 "count(*) AS " + MediaStore.Audio.Albums.NUMBER_OF_SONGS + 397 ",album_art._data AS album_art" + 398 " FROM audio LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id" + 399 " WHERE is_music=1 GROUP BY audio.album_id;"); 400 401 // For a given artist_id, provides the album_id for albums on 402 // which the artist appears. 403 db.execSQL("CREATE VIEW IF NOT EXISTS artists_albums_map AS " + 404 "SELECT DISTINCT artist_id, album_id FROM audio_meta;"); 405 406 /* 407 * Only external media volumes can handle genres, playlists, etc. 408 */ 409 if (!internal) { 410 // Cleans up when an audio file is deleted 411 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_meta_cleanup DELETE ON audio_meta " + 412 "BEGIN " + 413 "DELETE FROM audio_genres_map WHERE audio_id = old._id;" + 414 "DELETE FROM audio_playlists_map WHERE audio_id = old._id;" + 415 "END"); 416 417 // Contains audio genre definitions 418 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres (" + 419 "_id INTEGER PRIMARY KEY," + 420 "name TEXT NOT NULL" + 421 ");"); 422 423 // Contiains mappings between audio genres and audio files 424 db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres_map (" + 425 "_id INTEGER PRIMARY KEY," + 426 "audio_id INTEGER NOT NULL," + 427 "genre_id INTEGER NOT NULL" + 428 ");"); 429 430 // Cleans up when an audio genre is delete 431 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_genres_cleanup DELETE ON audio_genres " + 432 "BEGIN " + 433 "DELETE FROM audio_genres_map WHERE genre_id = old._id;" + 434 "END"); 435 436 // Contains audio playlist definitions 437 db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists (" + 438 "_id INTEGER PRIMARY KEY," + 439 "_data TEXT," + // _data is path for file based playlists, or null 440 "name TEXT NOT NULL," + 441 "date_added INTEGER," + 442 "date_modified INTEGER" + 443 ");"); 444 445 // Contains mappings between audio playlists and audio files 446 db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists_map (" + 447 "_id INTEGER PRIMARY KEY," + 448 "audio_id INTEGER NOT NULL," + 449 "playlist_id INTEGER NOT NULL," + 450 "play_order INTEGER NOT NULL" + 451 ");"); 452 453 // Cleans up when an audio playlist is deleted 454 db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_playlists_cleanup DELETE ON audio_playlists " + 455 "BEGIN " + 456 "DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" + 457 "SELECT _DELETE_FILE(old._data);" + 458 "END"); 459 460 // Cleans up album_art table entry when an album is deleted 461 db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup1 DELETE ON albums " + 462 "BEGIN " + 463 "DELETE FROM album_art WHERE album_id = old.album_id;" + 464 "END"); 465 466 // Cleans up album_art when an album is deleted 467 db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup2 DELETE ON album_art " + 468 "BEGIN " + 469 "SELECT _DELETE_FILE(old._data);" + 470 "END"); 471 } 472 473 // Contains meta data about video files 474 db.execSQL("CREATE TABLE IF NOT EXISTS video (" + 475 "_id INTEGER PRIMARY KEY," + 476 "_data TEXT NOT NULL," + 477 "_display_name TEXT," + 478 "_size INTEGER," + 479 "mime_type TEXT," + 480 "date_added INTEGER," + 481 "date_modified INTEGER," + 482 "title TEXT," + 483 "duration INTEGER," + 484 "artist TEXT," + 485 "album TEXT," + 486 "resolution TEXT," + 487 "description TEXT," + 488 "isprivate INTEGER," + // for YouTube videos 489 "tags TEXT," + // for YouTube videos 490 "category TEXT," + // for YouTube videos 491 "language TEXT," + // for YouTube videos 492 "mini_thumb_data TEXT," + 493 "latitude DOUBLE," + 494 "longitude DOUBLE," + 495 "datetaken INTEGER," + 496 "mini_thumb_magic INTEGER" + 497 ");"); 498 499 db.execSQL("CREATE TRIGGER IF NOT EXISTS video_cleanup DELETE ON video " + 500 "BEGIN " + 501 "SELECT _DELETE_FILE(old._data);" + 502 "END"); 503 } 504 505 // At this point the database is at least at schema version 63 (it was 506 // either created at version 63 by the code above, or was already at 507 // version 63 or later) 508 509 if (fromVersion < 64) { 510 // create the index that updates the database to schema version 64 511 db.execSQL("CREATE INDEX IF NOT EXISTS sort_index on images(datetaken ASC, _id ASC);"); 512 } 513 514 if (fromVersion < 65) { 515 // create the index that updates the database to schema version 65 516 db.execSQL("CREATE INDEX IF NOT EXISTS titlekey_index on audio_meta(title_key);"); 517 } 518 519 if (fromVersion < 66) { 520 updateBucketNames(db, "images"); 521 } 522 523 if (fromVersion < 67) { 524 // create the indices that update the database to schema version 67 525 db.execSQL("CREATE INDEX IF NOT EXISTS albumkey_index on albums(album_key);"); 526 db.execSQL("CREATE INDEX IF NOT EXISTS artistkey_index on artists(artist_key);"); 527 } 528 529 if (fromVersion < 68) { 530 // Create bucket_id and bucket_display_name columns for the video table. 531 db.execSQL("ALTER TABLE video ADD COLUMN bucket_id TEXT;"); 532 db.execSQL("ALTER TABLE video ADD COLUMN bucket_display_name TEXT"); 533 updateBucketNames(db, "video"); 534 } 535 536 if (fromVersion < 69) { 537 updateDisplayName(db, "images"); 538 } 539 540 if (fromVersion < 70) { 541 // Create bookmark column for the video table. 542 db.execSQL("ALTER TABLE video ADD COLUMN bookmark INTEGER;"); 543 } 544 545 if (fromVersion < 71) { 546 // There is no change to the database schema, however a code change 547 // fixed parsing of metadata for certain files bought from the 548 // iTunes music store, so we want to rescan files that might need it. 549 // We do this by clearing the modification date in the database for 550 // those files, so that the media scanner will see them as updated 551 // and rescan them. 552 db.execSQL("UPDATE audio_meta SET date_modified=0 WHERE _id IN (" + 553 "SELECT _id FROM audio where mime_type='audio/mp4' AND " + 554 "artist='" + MediaFile.UNKNOWN_STRING + "' AND " + 555 "album='" + MediaFile.UNKNOWN_STRING + "'" + 556 ");"); 557 } 558 } 559 560 /** 561 * Iterate through the rows of a table in a database, ensuring that the bucket_id and 562 * bucket_display_name columns are correct. 563 * @param db 564 * @param tableName 565 */ 566 private static void updateBucketNames(SQLiteDatabase db, String tableName) { 567 // Rebuild the bucket_display_name column using the natural case rather than lower case. 568 db.beginTransaction(); 569 try { 570 String[] columns = {BaseColumns._ID, MediaColumns.DATA}; 571 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 572 try { 573 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 574 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 575 while (cursor.moveToNext()) { 576 String data = cursor.getString(dataColumnIndex); 577 ContentValues values = new ContentValues(); 578 computeBucketValues(data, values); 579 int rowId = cursor.getInt(idColumnIndex); 580 db.update(tableName, values, "_id=" + rowId, null); 581 } 582 } finally { 583 cursor.close(); 584 } 585 db.setTransactionSuccessful(); 586 } finally { 587 db.endTransaction(); 588 } 589 } 590 591 /** 592 * Iterate through the rows of a table in a database, ensuring that the 593 * display name column has a value. 594 * @param db 595 * @param tableName 596 */ 597 private static void updateDisplayName(SQLiteDatabase db, String tableName) { 598 // Fill in default values for null displayName values 599 db.beginTransaction(); 600 try { 601 String[] columns = {BaseColumns._ID, MediaColumns.DATA, MediaColumns.DISPLAY_NAME}; 602 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 603 try { 604 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 605 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 606 final int displayNameIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME); 607 ContentValues values = new ContentValues(); 608 while (cursor.moveToNext()) { 609 String displayName = cursor.getString(displayNameIndex); 610 if (displayName == null) { 611 String data = cursor.getString(dataColumnIndex); 612 values.clear(); 613 computeDisplayName(data, values); 614 int rowId = cursor.getInt(idColumnIndex); 615 db.update(tableName, values, "_id=" + rowId, null); 616 } 617 } 618 } finally { 619 cursor.close(); 620 } 621 db.setTransactionSuccessful(); 622 } finally { 623 db.endTransaction(); 624 } 625 } 626 /** 627 * @param data The input path 628 * @param values the content values, where the bucked id name and bucket display name are updated. 629 * 630 */ 631 632 private static void computeBucketValues(String data, ContentValues values) { 633 File parentFile = new File(data).getParentFile(); 634 if (parentFile == null) { 635 parentFile = new File("/"); 636 } 637 638 // Lowercase the path for hashing. This avoids duplicate buckets if the 639 // filepath case is changed externally. 640 // Keep the original case for display. 641 String path = parentFile.toString().toLowerCase(); 642 String name = parentFile.getName(); 643 644 // Note: the BUCKET_ID and BUCKET_DISPLAY_NAME attributes are spelled the 645 // same for both images and video. However, for backwards-compatibility reasons 646 // there is no common base class. We use the ImageColumns version here 647 values.put(ImageColumns.BUCKET_ID, path.hashCode()); 648 values.put(ImageColumns.BUCKET_DISPLAY_NAME, name); 649 } 650 651 /** 652 * @param data The input path 653 * @param values the content values, where the display name is updated. 654 * 655 */ 656 private static void computeDisplayName(String data, ContentValues values) { 657 String s = (data == null ? "" : data.toString()); 658 int idx = s.lastIndexOf('/'); 659 if (idx >= 0) { 660 s = s.substring(idx + 1); 661 } 662 values.put("_display_name", s); 663 } 664 665 @Override 666 public Cursor query(Uri uri, String[] projectionIn, String selection, 667 String[] selectionArgs, String sort) { 668 int table = URI_MATCHER.match(uri); 669 670 // handle MEDIA_SCANNER before calling getDatabaseForUri() 671 if (table == MEDIA_SCANNER) { 672 if (mMediaScannerVolume == null) { 673 return null; 674 } else { 675 // create a cursor to return volume currently being scanned by the media scanner 676 return new MediaScannerCursor(mMediaScannerVolume); 677 } 678 } 679 680 String groupBy = null; 681 DatabaseHelper database = getDatabaseForUri(uri); 682 if (database == null) { 683 return null; 684 } 685 SQLiteDatabase db = database.getReadableDatabase(); 686 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 687 688 switch (table) { 689 case IMAGES_MEDIA: 690 qb.setTables("images"); 691 if (uri.getQueryParameter("distinct") != null) 692 qb.setDistinct(true); 693 694 // set the project map so that data dir is prepended to _data. 695 //qb.setProjectionMap(mImagesProjectionMap, true); 696 break; 697 698 case IMAGES_MEDIA_ID: 699 qb.setTables("images"); 700 if (uri.getQueryParameter("distinct") != null) 701 qb.setDistinct(true); 702 703 // set the project map so that data dir is prepended to _data. 704 //qb.setProjectionMap(mImagesProjectionMap, true); 705 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 706 break; 707 708 case IMAGES_THUMBNAILS: 709 qb.setTables("thumbnails"); 710 break; 711 712 case IMAGES_THUMBNAILS_ID: 713 qb.setTables("thumbnails"); 714 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 715 break; 716 717 case AUDIO_MEDIA: 718 qb.setTables("audio "); 719 break; 720 721 case AUDIO_MEDIA_ID: 722 qb.setTables("audio"); 723 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 724 break; 725 726 case AUDIO_MEDIA_ID_GENRES: 727 qb.setTables("audio_genres"); 728 qb.appendWhere("_id IN (SELECT genre_id FROM " + 729 "audio_genres_map WHERE audio_id = " + 730 uri.getPathSegments().get(3) + ")"); 731 break; 732 733 case AUDIO_MEDIA_ID_GENRES_ID: 734 qb.setTables("audio_genres"); 735 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 736 break; 737 738 case AUDIO_MEDIA_ID_PLAYLISTS: 739 qb.setTables("audio_playlists"); 740 qb.appendWhere("_id IN (SELECT playlist_id FROM " + 741 "audio_playlists_map WHERE audio_id = " + 742 uri.getPathSegments().get(3) + ")"); 743 break; 744 745 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 746 qb.setTables("audio_playlists"); 747 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 748 break; 749 750 case AUDIO_GENRES: 751 qb.setTables("audio_genres"); 752 break; 753 754 case AUDIO_GENRES_ID: 755 qb.setTables("audio_genres"); 756 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 757 break; 758 759 case AUDIO_GENRES_ID_MEMBERS: 760 qb.setTables("audio"); 761 qb.appendWhere("_id IN (SELECT audio_id FROM " + 762 "audio_genres_map WHERE genre_id = " + 763 uri.getPathSegments().get(3) + ")"); 764 break; 765 766 case AUDIO_GENRES_ID_MEMBERS_ID: 767 qb.setTables("audio"); 768 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 769 break; 770 771 case AUDIO_PLAYLISTS: 772 qb.setTables("audio_playlists"); 773 break; 774 775 case AUDIO_PLAYLISTS_ID: 776 qb.setTables("audio_playlists"); 777 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 778 break; 779 780 case AUDIO_PLAYLISTS_ID_MEMBERS: 781 for (int i = 0; i < projectionIn.length; i++) { 782 if (projectionIn[i].equals("_id")) { 783 projectionIn[i] = "audio_playlists_map._id AS _id"; 784 } 785 } 786 qb.setTables("audio_playlists_map, audio"); 787 qb.appendWhere("audio._id = audio_id AND playlist_id = " 788 + uri.getPathSegments().get(3)); 789 break; 790 791 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 792 qb.setTables("audio"); 793 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 794 break; 795 796 case VIDEO_MEDIA: 797 qb.setTables("video"); 798 break; 799 800 case VIDEO_MEDIA_ID: 801 qb.setTables("video"); 802 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 803 break; 804 805 case AUDIO_ARTISTS: 806 qb.setTables("artist_info"); 807 break; 808 809 case AUDIO_ARTISTS_ID: 810 qb.setTables("artist_info"); 811 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 812 break; 813 814 case AUDIO_ARTISTS_ID_ALBUMS: 815 String aid = uri.getPathSegments().get(3); 816 qb.setTables("audio LEFT OUTER JOIN album_art ON" + 817 " audio.album_id=album_art.album_id"); 818 qb.appendWhere("is_music=1 AND audio.album_id IN (SELECT album_id FROM " + 819 "artists_albums_map WHERE artist_id = " + 820 aid + ")"); 821 groupBy = "audio.album_id"; 822 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 823 "count(CASE WHEN artist_id==" + aid + " THEN 'foo' ELSE NULL END) AS " + 824 MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 825 qb.setProjectionMap(sArtistAlbumsMap); 826 break; 827 828 case AUDIO_ALBUMS: 829 qb.setTables("album_info"); 830 break; 831 832 case AUDIO_ALBUMS_ID: 833 qb.setTables("album_info"); 834 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 835 break; 836 837 case AUDIO_ALBUMART_ID: 838 qb.setTables("album_art"); 839 qb.appendWhere("album_id=" + uri.getPathSegments().get(3)); 840 break; 841 842 case AUDIO_SEARCH: 843 return doAudioSearch(db, qb, uri, projectionIn, selection, selectionArgs, sort); 844 845 default: 846 throw new IllegalStateException("Unknown URL: " + uri.toString()); 847 } 848 849 Cursor c = qb.query(db, projectionIn, selection, 850 selectionArgs, groupBy, null, sort); 851 if (c != null) { 852 c.setNotificationUri(getContext().getContentResolver(), uri); 853 } 854 return c; 855 } 856 857 private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb, 858 Uri uri, String[] projectionIn, String selection, 859 String[] selectionArgs, String sort) { 860 861 List<String> l = uri.getPathSegments(); 862 String mSearchString = l.size() == 4 ? l.get(3) : ""; 863 mSearchString = mSearchString.replaceAll(" ", " ").trim().toLowerCase(); 864 Cursor mCursor = null; 865 866 String [] searchWords = mSearchString.length() > 0 ? 867 mSearchString.split(" ") : new String[0]; 868 String [] wildcardWords3 = new String[searchWords.length * 3]; 869 Collator col = Collator.getInstance(); 870 col.setStrength(Collator.PRIMARY); 871 int len = searchWords.length; 872 for (int i = 0; i < len; i++) { 873 // Because we match on individual words here, we need to remove words 874 // like 'a' and 'the' that aren't part of the keys. 875 wildcardWords3[i] = wildcardWords3[i + len] = wildcardWords3[i + len + len] = 876 (searchWords[i].equals("a") || searchWords[i].equals("an") || 877 searchWords[i].equals("the")) ? "%" : 878 '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%'; 879 } 880 881 String UQs [] = new String[3]; 882 HashSet<String> tablecolumns = new HashSet<String>(); 883 884 // Direct match artists 885 { 886 String[] ccols = new String[] { 887 MediaStore.Audio.Artists._ID, 888 "'artist' AS " + MediaStore.Audio.Media.MIME_TYPE, 889 "" + R.drawable.ic_search_category_music_artist + " AS " + 890 SearchManager.SUGGEST_COLUMN_ICON_1, 891 "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2, 892 MediaStore.Audio.Artists.ARTIST + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 893 MediaStore.Audio.Artists.NUMBER_OF_ALBUMS + " AS data1", 894 MediaStore.Audio.Artists.NUMBER_OF_TRACKS + " AS data2", 895 MediaStore.Audio.Artists.ARTIST_KEY + " AS ar", // 896 "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 897 "'content://media/external/audio/artists/'||" + MediaStore.Audio.Artists._ID + 898 " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA, 899 "'1' AS grouporder", 900 "artist_key AS itemorder" 901 }; 902 903 904 String where = MediaStore.Audio.Artists.ARTIST_KEY + " != ''"; 905 for (int i = 0; i < searchWords.length; i++) { 906 where += " AND ar LIKE ?"; 907 } 908 909 qb.setTables("artist_info"); 910 UQs[0] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE, 911 ccols, tablecolumns, 12, "artist", where, null, null, null); 912 } 913 914 // Direct match albums 915 { 916 String[] ccols = new String[] { 917 MediaStore.Audio.Albums._ID, 918 "'album' AS " + MediaStore.Audio.Media.MIME_TYPE, 919 "" + R.drawable.ic_search_category_music_album + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1, 920 "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2, 921 MediaStore.Audio.Albums.ALBUM + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 922 MediaStore.Audio.Media.ARTIST + " AS data1", 923 "null AS data2", 924 MediaStore.Audio.Media.ARTIST_KEY + 925 "||' '||" + 926 MediaStore.Audio.Media.ALBUM_KEY + 927 " AS ar_al", 928 "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 929 "'content://media/external/audio/albums/'||" + MediaStore.Audio.Albums._ID + 930 " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA, 931 "'2' AS grouporder", 932 "album_key AS itemorder" 933 }; 934 935 String where = MediaStore.Audio.Media.ALBUM_KEY + " != ''"; 936 for (int i = 0; i < searchWords.length; i++) { 937 where += " AND ar_al LIKE ?"; 938 } 939 940 qb = new SQLiteQueryBuilder(); 941 qb.setTables("album_info"); 942 UQs[1] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE, 943 ccols, tablecolumns, 12, "album", where, null, null, null); 944 } 945 946 // Direct match tracks 947 { 948 String[] ccols = new String[] { 949 "audio._id AS _id", 950 MediaStore.Audio.Media.MIME_TYPE, 951 "" + R.drawable.ic_search_category_music_song + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1, 952 "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2, 953 MediaStore.Audio.Media.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 954 MediaStore.Audio.Media.ARTIST + " AS data1", 955 MediaStore.Audio.Media.ALBUM + " AS data2", 956 MediaStore.Audio.Media.ARTIST_KEY + 957 "||' '||" + 958 MediaStore.Audio.Media.ALBUM_KEY + 959 "||' '||" + 960 MediaStore.Audio.Media.TITLE_KEY + 961 " AS ar_al_ti", 962 "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 963 "'content://media/external/audio/media/'||audio._id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA, 964 "'3' AS grouporder", 965 "title_key AS itemorder" 966 }; 967 968 String where = MediaStore.Audio.Media.TITLE + " != ''"; 969 970 for (int i = 0; i < searchWords.length; i++) { 971 where += " AND ar_al_ti LIKE ?"; 972 } 973 qb = new SQLiteQueryBuilder(); 974 qb.setTables("audio"); 975 UQs[2] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE, 976 ccols, tablecolumns, 12, "audio/", where, null, null, null); 977 } 978 979 if (mCursor != null) { 980 mCursor.deactivate(); 981 mCursor = null; 982 } 983 if (UQs[0] != null && UQs[1] != null && UQs[2] != null) { 984 String union = qb.buildUnionQuery(UQs, "grouporder,itemorder", null); 985 mCursor = db.rawQuery(union, wildcardWords3); 986 } 987 988 return mCursor; 989 } 990 991 @Override 992 public String getType(Uri url) 993 { 994 switch (URI_MATCHER.match(url)) { 995 case IMAGES_MEDIA_ID: 996 case AUDIO_MEDIA_ID: 997 case AUDIO_GENRES_ID_MEMBERS_ID: 998 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 999 case VIDEO_MEDIA_ID: 1000 Cursor c = query(url, MIME_TYPE_PROJECTION, null, null, null); 1001 if (c != null && c.getCount() == 1) { 1002 c.moveToFirst(); 1003 String mimeType = c.getString(1); 1004 c.deactivate(); 1005 return mimeType; 1006 } 1007 break; 1008 1009 case IMAGES_MEDIA: 1010 case IMAGES_THUMBNAILS: 1011 return Images.Media.CONTENT_TYPE; 1012 case IMAGES_THUMBNAILS_ID: 1013 return "image/jpeg"; 1014 1015 case AUDIO_MEDIA: 1016 case AUDIO_GENRES_ID_MEMBERS: 1017 case AUDIO_PLAYLISTS_ID_MEMBERS: 1018 return Audio.Media.CONTENT_TYPE; 1019 1020 case AUDIO_GENRES: 1021 case AUDIO_MEDIA_ID_GENRES: 1022 return Audio.Genres.CONTENT_TYPE; 1023 case AUDIO_GENRES_ID: 1024 case AUDIO_MEDIA_ID_GENRES_ID: 1025 return Audio.Genres.ENTRY_CONTENT_TYPE; 1026 case AUDIO_PLAYLISTS: 1027 case AUDIO_MEDIA_ID_PLAYLISTS: 1028 return Audio.Playlists.CONTENT_TYPE; 1029 case AUDIO_PLAYLISTS_ID: 1030 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1031 return Audio.Playlists.ENTRY_CONTENT_TYPE; 1032 1033 case VIDEO_MEDIA: 1034 return Video.Media.CONTENT_TYPE; 1035 } 1036 throw new IllegalStateException("Unknown URL"); 1037 } 1038 1039 /** 1040 * Ensures there is a file in the _data column of values, if one isn't 1041 * present a new file is created. 1042 * 1043 * @param initialValues the values passed to insert by the caller 1044 * @return the new values 1045 */ 1046 private ContentValues ensureFile(boolean internal, ContentValues initialValues, 1047 String preferredExtension, String directoryName) { 1048 ContentValues values; 1049 String file = initialValues.getAsString("_data"); 1050 if (TextUtils.isEmpty(file)) { 1051 file = generateFileName(internal, preferredExtension, directoryName); 1052 values = new ContentValues(initialValues); 1053 values.put("_data", file); 1054 } else { 1055 values = initialValues; 1056 } 1057 1058 if (!ensureFileExists(file)) { 1059 throw new IllegalStateException("Unable to create new file: " + file); 1060 } 1061 return values; 1062 } 1063 1064 @Override 1065 public int bulkInsert(Uri uri, ContentValues values[]) { 1066 int match = URI_MATCHER.match(uri); 1067 if (match == VOLUMES) { 1068 return super.bulkInsert(uri, values); 1069 } 1070 DatabaseHelper database = getDatabaseForUri(uri); 1071 if (database == null) { 1072 throw new UnsupportedOperationException( 1073 "Unknown URI: " + uri); 1074 } 1075 SQLiteDatabase db = database.getWritableDatabase(); 1076 db.beginTransaction(); 1077 int numInserted = 0; 1078 try { 1079 int len = values.length; 1080 for (int i = 0; i < len; i++) { 1081 insertInternal(uri, values[i]); 1082 } 1083 numInserted = len; 1084 db.setTransactionSuccessful(); 1085 } finally { 1086 db.endTransaction(); 1087 } 1088 getContext().getContentResolver().notifyChange(uri, null); 1089 return numInserted; 1090 } 1091 1092 @Override 1093 public Uri insert(Uri uri, ContentValues initialValues) 1094 { 1095 Uri newUri = insertInternal(uri, initialValues); 1096 if (newUri != null) { 1097 getContext().getContentResolver().notifyChange(uri, null); 1098 } 1099 1100 return newUri; 1101 } 1102 1103 private Uri insertInternal(Uri uri, ContentValues initialValues) { 1104 long rowId; 1105 int match = URI_MATCHER.match(uri); 1106 1107 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1108 if (match == MEDIA_SCANNER) { 1109 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 1110 return MediaStore.getMediaScannerUri(); 1111 } 1112 1113 Uri newUri = null; 1114 DatabaseHelper database = getDatabaseForUri(uri); 1115 if (database == null && match != VOLUMES) { 1116 throw new UnsupportedOperationException( 1117 "Unknown URI: " + uri); 1118 } 1119 SQLiteDatabase db = (match == VOLUMES ? null : database.getWritableDatabase()); 1120 1121 if (initialValues == null) { 1122 initialValues = new ContentValues(); 1123 } 1124 1125 switch (match) { 1126 case IMAGES_MEDIA: { 1127 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/Camera"); 1128 1129 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1130 String data = values.getAsString(MediaColumns.DATA); 1131 if (! values.containsKey(MediaColumns.DISPLAY_NAME)) { 1132 computeDisplayName(data, values); 1133 } 1134 computeBucketValues(data, values); 1135 rowId = db.insert("images", "name", values); 1136 1137 if (rowId > 0) { 1138 newUri = ContentUris.withAppendedId( 1139 Images.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 1140 } 1141 break; 1142 } 1143 1144 case IMAGES_THUMBNAILS: { 1145 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/.thumbnails"); 1146 rowId = db.insert("thumbnails", "name", values); 1147 if (rowId > 0) { 1148 newUri = ContentUris.withAppendedId(Images.Thumbnails. 1149 getContentUri(uri.getPathSegments().get(0)), rowId); 1150 } 1151 break; 1152 } 1153 1154 case AUDIO_MEDIA: { 1155 // SQLite Views are read-only, so we need to deconstruct this 1156 // insert and do inserts into the underlying tables. 1157 // If doing this here turns out to be a performance bottleneck, 1158 // consider moving this to native code and using triggers on 1159 // the view. 1160 ContentValues values = new ContentValues(initialValues); 1161 1162 // Insert the artist into the artist table and remove it from 1163 // the input values 1164 Object so = values.get("artist"); 1165 String s = (so == null ? "" : so.toString()); 1166 values.remove("artist"); 1167 long artistRowId; 1168 HashMap<String, Long> artistCache = database.mArtistCache; 1169 synchronized(artistCache) { 1170 Long temp = artistCache.get(s); 1171 if (temp == null) { 1172 artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist", 1173 s, null, artistCache, uri); 1174 } else { 1175 artistRowId = temp.longValue(); 1176 } 1177 } 1178 1179 // Do the same for the album field 1180 so = values.get("album"); 1181 s = (so == null ? "" : so.toString()); 1182 values.remove("album"); 1183 long albumRowId; 1184 HashMap<String, Long> albumCache = database.mAlbumCache; 1185 synchronized(albumCache) { 1186 Long temp = albumCache.get(s); 1187 if (temp == null) { 1188 String path = values.getAsString("_data"); 1189 albumRowId = getKeyIdForName(db, "albums", "album_key", "album", 1190 s, path, albumCache, uri); 1191 } else { 1192 albumRowId = temp; 1193 } 1194 } 1195 1196 values.put("artist_id", Integer.toString((int)artistRowId)); 1197 values.put("album_id", Integer.toString((int)albumRowId)); 1198 so = values.getAsString("title"); 1199 s = (so == null ? "" : so.toString()); 1200 values.put("title_key", MediaStore.Audio.keyFor(s)); 1201 1202 computeDisplayName(values.getAsString("_data"), values); 1203 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1204 1205 rowId = db.insert("audio_meta", "duration", values); 1206 if (rowId > 0) { 1207 newUri = ContentUris.withAppendedId(Audio.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 1208 } 1209 break; 1210 } 1211 1212 case AUDIO_MEDIA_ID_GENRES: { 1213 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 1214 ContentValues values = new ContentValues(initialValues); 1215 values.put(Audio.Genres.Members.AUDIO_ID, audioId); 1216 rowId = db.insert("audio_playlists_map", "genre_id", values); 1217 if (rowId > 0) { 1218 newUri = ContentUris.withAppendedId(uri, rowId); 1219 } 1220 break; 1221 } 1222 1223 case AUDIO_MEDIA_ID_PLAYLISTS: { 1224 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 1225 ContentValues values = new ContentValues(initialValues); 1226 values.put(Audio.Playlists.Members.AUDIO_ID, audioId); 1227 rowId = db.insert("audio_playlists_map", "playlist_id", 1228 values); 1229 if (rowId > 0) { 1230 newUri = ContentUris.withAppendedId(uri, rowId); 1231 } 1232 break; 1233 } 1234 1235 case AUDIO_GENRES: { 1236 rowId = db.insert("audio_genres", "audio_id", initialValues); 1237 if (rowId > 0) { 1238 newUri = ContentUris.withAppendedId(Audio.Genres.getContentUri(uri.getPathSegments().get(0)), rowId); 1239 } 1240 break; 1241 } 1242 1243 case AUDIO_GENRES_ID_MEMBERS: { 1244 Long genreId = Long.parseLong(uri.getPathSegments().get(3)); 1245 ContentValues values = new ContentValues(initialValues); 1246 values.put(Audio.Genres.Members.GENRE_ID, genreId); 1247 rowId = db.insert("audio_genres_map", "genre_id", values); 1248 if (rowId > 0) { 1249 newUri = ContentUris.withAppendedId(uri, rowId); 1250 } 1251 break; 1252 } 1253 1254 case AUDIO_PLAYLISTS: { 1255 ContentValues values = new ContentValues(initialValues); 1256 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 1257 rowId = db.insert("audio_playlists", "name", initialValues); 1258 if (rowId > 0) { 1259 newUri = ContentUris.withAppendedId(Audio.Playlists.getContentUri(uri.getPathSegments().get(0)), rowId); 1260 } 1261 break; 1262 } 1263 1264 case AUDIO_PLAYLISTS_ID: 1265 case AUDIO_PLAYLISTS_ID_MEMBERS: { 1266 Long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 1267 ContentValues values = new ContentValues(initialValues); 1268 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId); 1269 rowId = db.insert("audio_playlists_map", "playlist_id", 1270 values); 1271 if (rowId > 0) { 1272 newUri = ContentUris.withAppendedId(uri, rowId); 1273 } 1274 break; 1275 } 1276 1277 case VIDEO_MEDIA: { 1278 ContentValues values = ensureFile(database.mInternal, initialValues, ".3gp", "video"); 1279 String data = values.getAsString("_data"); 1280 computeDisplayName(data, values); 1281 computeBucketValues(data, values); 1282 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1283 rowId = db.insert("video", "artist", values); 1284 if (rowId > 0) { 1285 newUri = ContentUris.withAppendedId(Video.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 1286 } 1287 break; 1288 } 1289 1290 case AUDIO_ALBUMART: 1291 if (database.mInternal) { 1292 throw new UnsupportedOperationException("no internal album art allowed"); 1293 } 1294 ContentValues values = null; 1295 try { 1296 values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 1297 } catch (IllegalStateException ex) { 1298 // probably no more room to store albumthumbs 1299 values = initialValues; 1300 } 1301 rowId = db.insert("album_art", "_data", values); 1302 if (rowId > 0) { 1303 newUri = ContentUris.withAppendedId(uri, rowId); 1304 } 1305 break; 1306 1307 case VOLUMES: 1308 return attachVolume(initialValues.getAsString("name")); 1309 1310 default: 1311 throw new UnsupportedOperationException("Invalid URI " + uri); 1312 } 1313 1314 return newUri; 1315 } 1316 1317 private String generateFileName(boolean internal, String preferredExtension, String directoryName) 1318 { 1319 // create a random file 1320 String name = String.valueOf(System.currentTimeMillis()); 1321 1322 if (internal) { 1323 throw new UnsupportedOperationException("Writing to internal storage is not supported."); 1324// return Environment.getDataDirectory() 1325// + "/" + directoryName + "/" + name + preferredExtension; 1326 } else { 1327 return Environment.getExternalStorageDirectory() 1328 + "/" + directoryName + "/" + name + preferredExtension; 1329 } 1330 } 1331 1332 private boolean ensureFileExists(String path) { 1333 File file = new File(path); 1334 if (file.exists()) { 1335 return true; 1336 } else { 1337 // we will not attempt to create the first directory in the path 1338 // (for example, do not create /sdcard if the SD card is not mounted) 1339 int secondSlash = path.indexOf('/', 1); 1340 if (secondSlash < 1) return false; 1341 String directoryPath = path.substring(0, secondSlash); 1342 File directory = new File(directoryPath); 1343 if (!directory.exists()) 1344 return false; 1345 file.getParentFile().mkdirs(); 1346 try { 1347 return file.createNewFile(); 1348 } catch(IOException ioe) { 1349 Log.e(TAG, "File creation failed", ioe); 1350 } 1351 return false; 1352 } 1353 } 1354 1355 private static final class GetTableAndWhereOutParameter { 1356 public String table; 1357 public String where; 1358 } 1359 1360 static final GetTableAndWhereOutParameter sGetTableAndWhereParam = 1361 new GetTableAndWhereOutParameter(); 1362 1363 private void getTableAndWhere(Uri uri, int match, String userWhere, 1364 GetTableAndWhereOutParameter out) { 1365 String where = null; 1366 switch (match) { 1367 case IMAGES_MEDIA_ID: 1368 out.table = "images"; 1369 where = "_id = " + uri.getPathSegments().get(3); 1370 break; 1371 1372 case AUDIO_MEDIA: 1373 out.table = "audio"; 1374 break; 1375 1376 case AUDIO_MEDIA_ID: 1377 out.table = "audio"; 1378 where = "_id=" + uri.getPathSegments().get(3); 1379 break; 1380 1381 case AUDIO_MEDIA_ID_GENRES: 1382 out.table = "audio_genres"; 1383 where = "audio_id=" + uri.getPathSegments().get(3); 1384 break; 1385 1386 case AUDIO_MEDIA_ID_GENRES_ID: 1387 out.table = "audio_genres"; 1388 where = "audio_id=" + uri.getPathSegments().get(3) + 1389 " AND genre_id=" + uri.getPathSegments().get(5); 1390 break; 1391 1392 case AUDIO_MEDIA_ID_PLAYLISTS: 1393 out.table = "audio_playlists"; 1394 where = "audio_id=" + uri.getPathSegments().get(3); 1395 break; 1396 1397 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1398 out.table = "audio_playlists"; 1399 where = "audio_id=" + uri.getPathSegments().get(3) + 1400 " AND playlists_id=" + uri.getPathSegments().get(5); 1401 break; 1402 1403 case AUDIO_GENRES: 1404 out.table = "audio_genres"; 1405 break; 1406 1407 case AUDIO_GENRES_ID: 1408 out.table = "audio_genres"; 1409 where = "_id=" + uri.getPathSegments().get(3); 1410 break; 1411 1412 case AUDIO_GENRES_ID_MEMBERS: 1413 out.table = "audio_genres"; 1414 where = "genre_id=" + uri.getPathSegments().get(3); 1415 break; 1416 1417 case AUDIO_GENRES_ID_MEMBERS_ID: 1418 out.table = "audio_genres"; 1419 where = "genre_id=" + uri.getPathSegments().get(3) + 1420 " AND audio_id =" + uri.getPathSegments().get(5); 1421 break; 1422 1423 case AUDIO_PLAYLISTS: 1424 out.table = "audio_playlists"; 1425 break; 1426 1427 case AUDIO_PLAYLISTS_ID: 1428 out.table = "audio_playlists"; 1429 where = "_id=" + uri.getPathSegments().get(3); 1430 break; 1431 1432 case AUDIO_PLAYLISTS_ID_MEMBERS: 1433 out.table = "audio_playlists_map"; 1434 where = "playlist_id=" + uri.getPathSegments().get(3); 1435 break; 1436 1437 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1438 out.table = "audio_playlists_map"; 1439 where = "playlist_id=" + uri.getPathSegments().get(3) + 1440 " AND _id=" + uri.getPathSegments().get(5); 1441 break; 1442 1443 case AUDIO_ALBUMART_ID: 1444 out.table = "album_art"; 1445 where = "album_id=" + uri.getPathSegments().get(3); 1446 break; 1447 1448 case VIDEO_MEDIA: 1449 out.table = "video"; 1450 break; 1451 1452 case VIDEO_MEDIA_ID: 1453 out.table = "video"; 1454 where = "_id=" + uri.getPathSegments().get(3); 1455 break; 1456 1457 default: 1458 throw new UnsupportedOperationException( 1459 "Unknown or unsupported URL: " + uri.toString()); 1460 } 1461 1462 // Add in the user requested WHERE clause, if needed 1463 if (!TextUtils.isEmpty(userWhere)) { 1464 if (!TextUtils.isEmpty(where)) { 1465 out.where = where + " AND (" + userWhere + ")"; 1466 } else { 1467 out.where = userWhere; 1468 } 1469 } else { 1470 out.where = where; 1471 } 1472 } 1473 1474 @Override 1475 public int delete(Uri uri, String userWhere, String[] whereArgs) { 1476 int count; 1477 int match = URI_MATCHER.match(uri); 1478 1479 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1480 if (match == MEDIA_SCANNER) { 1481 if (mMediaScannerVolume == null) { 1482 return 0; 1483 } 1484 mMediaScannerVolume = null; 1485 return 1; 1486 } 1487 1488 if (match != VOLUMES_ID) { 1489 DatabaseHelper database = getDatabaseForUri(uri); 1490 if (database == null) { 1491 throw new UnsupportedOperationException( 1492 "Unknown URI: " + uri); 1493 } 1494 SQLiteDatabase db = database.getWritableDatabase(); 1495 1496 synchronized (sGetTableAndWhereParam) { 1497 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 1498 switch (match) { 1499 case AUDIO_MEDIA: 1500 case AUDIO_MEDIA_ID: 1501 count = db.delete("audio_meta", 1502 sGetTableAndWhereParam.where, whereArgs); 1503 break; 1504 default: 1505 count = db.delete(sGetTableAndWhereParam.table, 1506 sGetTableAndWhereParam.where, whereArgs); 1507 break; 1508 } 1509 getContext().getContentResolver().notifyChange(uri, null); 1510 } 1511 } else { 1512 detachVolume(uri); 1513 count = 1; 1514 } 1515 1516 return count; 1517 } 1518 1519 @Override 1520 public int update(Uri uri, ContentValues initialValues, String userWhere, 1521 String[] whereArgs) { 1522 int count; 1523 int match = URI_MATCHER.match(uri); 1524 1525 DatabaseHelper database = getDatabaseForUri(uri); 1526 if (database == null) { 1527 throw new UnsupportedOperationException( 1528 "Unknown URI: " + uri); 1529 } 1530 SQLiteDatabase db = database.getWritableDatabase(); 1531 1532 synchronized (sGetTableAndWhereParam) { 1533 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 1534 1535 switch (match) { 1536 case AUDIO_MEDIA: 1537 case AUDIO_MEDIA_ID: 1538 { 1539 ContentValues values = new ContentValues(initialValues); 1540 // Insert the artist into the artist table and remove it from 1541 // the input values 1542 String so = values.getAsString("artist"); 1543 if (so != null) { 1544 String s = so.toString(); 1545 values.remove("artist"); 1546 long artistRowId; 1547 HashMap<String, Long> artistCache = database.mArtistCache; 1548 synchronized(artistCache) { 1549 Long temp = artistCache.get(s); 1550 if (temp == null) { 1551 artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist", 1552 s, null, artistCache, uri); 1553 } else { 1554 artistRowId = temp.longValue(); 1555 } 1556 } 1557 values.put("artist_id", Integer.toString((int)artistRowId)); 1558 } 1559 1560 // Do the same for the album field 1561 so = values.getAsString("album"); 1562 if (so != null) { 1563 String s = so.toString(); 1564 values.remove("album"); 1565 long albumRowId; 1566 HashMap<String, Long> albumCache = database.mAlbumCache; 1567 synchronized(albumCache) { 1568 Long temp = albumCache.get(s); 1569 if (temp == null) { 1570 albumRowId = getKeyIdForName(db, "albums", "album_key", "album", 1571 s, null, albumCache, uri); 1572 } else { 1573 albumRowId = temp.longValue(); 1574 } 1575 } 1576 values.put("album_id", Integer.toString((int)albumRowId)); 1577 } 1578 1579 // don't allow the title_key field to be updated directly 1580 values.remove("title_key"); 1581 // If the title field is modified, update the title_key 1582 so = values.getAsString("title"); 1583 if (so != null) { 1584 String s = so.toString(); 1585 values.put("title_key", MediaStore.Audio.keyFor(s)); 1586 } 1587 1588 count = db.update("audio_meta", values, sGetTableAndWhereParam.where, 1589 whereArgs); 1590 } 1591 break; 1592 case IMAGES_MEDIA: 1593 case IMAGES_MEDIA_ID: 1594 case VIDEO_MEDIA: 1595 case VIDEO_MEDIA_ID: 1596 { 1597 ContentValues values = new ContentValues(initialValues); 1598 // Don't allow bucket id or display name to be updated directly. 1599 // The same names are used for both images and table columns, so 1600 // we use the ImageColumns constants here. 1601 values.remove(ImageColumns.BUCKET_ID); 1602 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 1603 // If the data is being modified update the bucket values 1604 String data = values.getAsString(MediaColumns.DATA); 1605 if (data != null) { 1606 computeBucketValues(data, values); 1607 } 1608 count = db.update(sGetTableAndWhereParam.table, values, 1609 sGetTableAndWhereParam.where, whereArgs); 1610 } 1611 break; 1612 default: 1613 count = db.update(sGetTableAndWhereParam.table, initialValues, 1614 sGetTableAndWhereParam.where, whereArgs); 1615 break; 1616 } 1617 } 1618 if (count > 0) { 1619 getContext().getContentResolver().notifyChange(uri, null); 1620 } 1621 return count; 1622 } 1623 1624 private static final String[] openFileColumns = new String[] { 1625 MediaStore.MediaColumns.DATA, 1626 }; 1627 1628 @Override 1629 public ParcelFileDescriptor openFile(Uri uri, String mode) 1630 throws FileNotFoundException { 1631 ParcelFileDescriptor pfd = null; 1632 try { 1633 pfd = openFileHelper(uri, mode); 1634 } catch (FileNotFoundException ex) { 1635 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_ID) { 1636 // Tried to open an album art file which does not exist. Regenerate. 1637 DatabaseHelper database = getDatabaseForUri(uri); 1638 if (database == null) { 1639 throw ex; 1640 } 1641 SQLiteDatabase db = database.getReadableDatabase(); 1642 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1643 int albumid = Integer.parseInt(uri.getPathSegments().get(3)); 1644 qb.setTables("audio"); 1645 qb.appendWhere("album_id=" + albumid); 1646 Cursor c = qb.query(db, 1647 new String [] { 1648 MediaStore.Audio.Media.DATA }, 1649 null, null, null, null, null); 1650 c.moveToFirst(); 1651 if (!c.isAfterLast()) { 1652 String audiopath = c.getString(0); 1653 makeThumb(db, audiopath, albumid, uri); 1654 } 1655 c.close(); 1656 } 1657 throw ex; 1658 } 1659 return pfd; 1660 } 1661 1662 private class Worker implements Runnable { 1663 private final Object mLock = new Object(); 1664 private Looper mLooper; 1665 1666 Worker(String name) { 1667 Thread t = new Thread(null, this, name); 1668 t.setPriority(Thread.MIN_PRIORITY); 1669 t.start(); 1670 synchronized (mLock) { 1671 while (mLooper == null) { 1672 try { 1673 mLock.wait(); 1674 } catch (InterruptedException ex) { 1675 } 1676 } 1677 } 1678 } 1679 1680 public Looper getLooper() { 1681 return mLooper; 1682 } 1683 1684 public void run() { 1685 synchronized (mLock) { 1686 Looper.prepare(); 1687 mLooper = Looper.myLooper(); 1688 mLock.notifyAll(); 1689 } 1690 Looper.loop(); 1691 } 1692 1693 public void quit() { 1694 mLooper.quit(); 1695 } 1696 } 1697 1698 private class ThumbData { 1699 SQLiteDatabase db; 1700 String path; 1701 long album_id; 1702 Uri albumart_uri; 1703 } 1704 1705 private void makeThumb(SQLiteDatabase db, String path, long album_id, 1706 Uri albumart_uri) { 1707 ThumbData d = new ThumbData(); 1708 d.db = db; 1709 d.path = path; 1710 d.album_id = album_id; 1711 d.albumart_uri = albumart_uri; 1712 Message msg = mThumbHandler.obtainMessage(); 1713 msg.obj = d; 1714 msg.sendToTarget(); 1715 } 1716 1717 private void makeThumb(ThumbData d) { 1718 SQLiteDatabase db = d.db; 1719 String path = d.path; 1720 long album_id = d.album_id; 1721 Uri albumart_uri = d.albumart_uri; 1722 1723 try { 1724 File f = new File(path); 1725 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, 1726 ParcelFileDescriptor.MODE_READ_ONLY); 1727 1728 MediaScanner scanner = new MediaScanner(getContext()); 1729 byte [] art = scanner.extractAlbumArt(pfd.getFileDescriptor()); 1730 pfd.close(); 1731 1732 // if no embedded art exists, look for AlbumArt.jpg in same directory as the media file 1733 if (art == null && path != null) { 1734 int lastSlash = path.lastIndexOf('/'); 1735 if (lastSlash > 0) { 1736 String artPath = path.substring(0, lastSlash + 1) + "AlbumArt.jpg"; 1737 File file = new File(artPath); 1738 if (file.exists()) { 1739 art = new byte[(int)file.length()]; 1740 FileInputStream stream = null; 1741 try { 1742 stream = new FileInputStream(file); 1743 stream.read(art); 1744 } catch (IOException ex) { 1745 art = null; 1746 } finally { 1747 if (stream != null) { 1748 stream.close(); 1749 } 1750 } 1751 } 1752 } 1753 } 1754 1755 Bitmap bm = null; 1756 if (art != null) { 1757 try { 1758 // get the size of the bitmap 1759 BitmapFactory.Options opts = new BitmapFactory.Options(); 1760 opts.inJustDecodeBounds = true; 1761 opts.inSampleSize = 1; 1762 BitmapFactory.decodeByteArray(art, 0, art.length, opts); 1763 1764 // request a reasonably sized output image 1765 // TODO: don't hardcode the size 1766 while (opts.outHeight > 320 || opts.outWidth > 320) { 1767 opts.outHeight /= 2; 1768 opts.outWidth /= 2; 1769 opts.inSampleSize *= 2; 1770 } 1771 1772 // get the image for real now 1773 opts.inJustDecodeBounds = false; 1774 opts.inPreferredConfig = Bitmap.Config.RGB_565; 1775 bm = BitmapFactory.decodeByteArray(art, 0, art.length, opts); 1776 } catch (Exception e) { 1777 } 1778 } 1779 if (bm != null && bm.getConfig() == null) { 1780 bm = bm.copy(Bitmap.Config.RGB_565, false); 1781 } 1782 if (bm != null) { 1783 // save bitmap 1784 Uri out = null; 1785 // TODO: this could be done more efficiently with a call to db.replace(), which 1786 // replaces or inserts as needed, making it unnecessary to query() first. 1787 if (albumart_uri != null) { 1788 Cursor c = query(albumart_uri, new String [] { "_data" }, 1789 null, null, null); 1790 c.moveToFirst(); 1791 if (!c.isAfterLast()) { 1792 String albumart_path = c.getString(0); 1793 if (ensureFileExists(albumart_path)) { 1794 out = albumart_uri; 1795 } 1796 } 1797 c.close(); 1798 } else { 1799 ContentValues initialValues = new ContentValues(); 1800 initialValues.put("album_id", album_id); 1801 try { 1802 ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 1803 long rowId = db.insert("album_art", "_data", values); 1804 if (rowId > 0) { 1805 out = ContentUris.withAppendedId(ALBUMART_URI, rowId); 1806 } 1807 } catch (IllegalStateException ex) { 1808 Log.e(TAG, "error creating album thumb file"); 1809 } 1810 } 1811 if (out != null) { 1812 boolean success = false; 1813 try { 1814 OutputStream outstream = getContext().getContentResolver().openOutputStream(out); 1815 success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream); 1816 outstream.close(); 1817 } catch (FileNotFoundException ex) { 1818 Log.e(TAG, "error creating file", ex); 1819 } catch (IOException ex) { 1820 Log.e(TAG, "error creating file", ex); 1821 } 1822 if (!success) { 1823 // the thumbnail was not written successfully, delete the entry that refers to it 1824 getContext().getContentResolver().delete(out, null, null); 1825 } 1826 } 1827 getContext().getContentResolver().notifyChange(MEDIA_URI, null); 1828 } 1829 } catch (IOException ex) { 1830 } 1831 1832 } 1833 1834 /** 1835 * Look up the artist or album entry for the given name, creating that entry 1836 * if it does not already exists. 1837 * @param db The database 1838 * @param table The table to store the key/name pair in. 1839 * @param keyField The name of the key-column 1840 * @param nameField The name of the name-column 1841 * @param rawName The name that the calling app was trying to insert into the database 1842 * @param path The path to the file being inserted in to the audio table 1843 * @param cache The cache to add this entry to 1844 * @param srcuri The Uri that prompted the call to this method, used for determining whether this is 1845 * the internal or external database 1846 * @return The row ID for this artist/album, or -1 if the provided name was invalid 1847 */ 1848 private long getKeyIdForName(SQLiteDatabase db, String table, String keyField, String nameField, 1849 String rawName, String path, HashMap<String, Long> cache, Uri srcuri) { 1850 long rowId; 1851 1852 if (rawName == null || rawName.length() == 0) { 1853 return -1; 1854 } 1855 String k = MediaStore.Audio.keyFor(rawName); 1856 1857 if (k == null) { 1858 return -1; 1859 } 1860 1861 String [] selargs = { k }; 1862 Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null); 1863 1864 try { 1865 switch (c.getCount()) { 1866 case 0: { 1867 // insert new entry into table 1868 ContentValues otherValues = new ContentValues(); 1869 otherValues.put(keyField, k); 1870 otherValues.put(nameField, rawName); 1871 rowId = db.insert(table, "duration", otherValues); 1872 if (path != null && table.equals("albums") && 1873 ! rawName.equals(MediaFile.UNKNOWN_STRING)) { 1874 // We just inserted a new album. Now create an album art thumbnail for it. 1875 makeThumb(db, path, rowId, null); 1876 } 1877 if (rowId > 0) { 1878 String volume = srcuri.toString().substring(16, 24); // extract internal/external 1879 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 1880 getContext().getContentResolver().notifyChange(uri, null); 1881 } 1882 } 1883 break; 1884 case 1: { 1885 // Use the existing entry 1886 c.moveToFirst(); 1887 rowId = c.getLong(0); 1888 1889 // Determine whether the current rawName is better than what's 1890 // currently stored in the table, and update the table if it is. 1891 String currentFancyName = c.getString(2); 1892 String bestName = makeBestName(rawName, currentFancyName); 1893 if (!bestName.equals(currentFancyName)) { 1894 // update the table with the new name 1895 ContentValues newValues = new ContentValues(); 1896 newValues.put(nameField, bestName); 1897 db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null); 1898 String volume = srcuri.toString().substring(16, 24); // extract internal/external 1899 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 1900 getContext().getContentResolver().notifyChange(uri, null); 1901 } 1902 } 1903 break; 1904 default: 1905 // corrupt database 1906 Log.e(TAG, "Multiple entries in table " + table + " for key " + k); 1907 rowId = -1; 1908 break; 1909 } 1910 } finally { 1911 if (c != null) c.close(); 1912 } 1913 1914 if (cache != null && ! rawName.equals(MediaFile.UNKNOWN_STRING)) { 1915 cache.put(rawName, rowId); 1916 } 1917 return rowId; 1918 } 1919 1920 /** 1921 * Returns the best string to use for display, given two names. 1922 * Note that this function does not necessarily return either one 1923 * of the provided names; it may decide to return a better alternative 1924 * (for example, specifying the inputs "Police" and "Police, The" will 1925 * return "The Police") 1926 * 1927 * The basic assumptions are: 1928 * - longer is better ("The police" is better than "Police") 1929 * - prefix is better ("The Police" is better than "Police, The") 1930 * - accents are better ("Motörhead" is better than "Motorhead") 1931 * 1932 * @param one The first of the two names to consider 1933 * @param two The last of the two names to consider 1934 * @return The actual name to use 1935 */ 1936 String makeBestName(String one, String two) { 1937 String name; 1938 1939 // Longer names are usually better. 1940 if (one.length() > two.length()) { 1941 name = one; 1942 } else { 1943 // Names with accents are usually better, and conveniently sort later 1944 if (one.toLowerCase().compareTo(two.toLowerCase()) > 0) { 1945 name = one; 1946 } else { 1947 name = two; 1948 } 1949 } 1950 1951 // Prefixes are better than postfixes. 1952 if (name.endsWith(", the") || name.endsWith(",the") || 1953 name.endsWith(", an") || name.endsWith(",an") || 1954 name.endsWith(", a") || name.endsWith(",a")) { 1955 String fix = name.substring(1 + name.lastIndexOf(',')); 1956 name = fix.trim() + " " + name.substring(0, name.lastIndexOf(',')); 1957 } 1958 1959 // TODO: word-capitalize the resulting name 1960 return name; 1961 } 1962 1963 1964 /** 1965 * Looks up the database based on the given URI. 1966 * 1967 * @param uri The requested URI 1968 * @returns the database for the given URI 1969 */ 1970 private DatabaseHelper getDatabaseForUri(Uri uri) { 1971 synchronized (mDatabases) { 1972 if (uri.getPathSegments().size() > 1) { 1973 return mDatabases.get(uri.getPathSegments().get(0)); 1974 } 1975 } 1976 return null; 1977 } 1978 1979 /** 1980 * Attach the database for a volume (internal or external). 1981 * Does nothing if the volume is already attached, otherwise 1982 * checks the volume ID and sets up the corresponding database. 1983 * 1984 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}. 1985 * @return the content URI of the attached volume. 1986 */ 1987 private Uri attachVolume(String volume) { 1988 if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) { 1989 throw new SecurityException( 1990 "Opening and closing databases not allowed."); 1991 } 1992 1993 synchronized (mDatabases) { 1994 if (mDatabases.get(volume) != null) { // Already attached 1995 return Uri.parse("content://media/" + volume); 1996 } 1997 1998 DatabaseHelper db; 1999 if (INTERNAL_VOLUME.equals(volume)) { 2000 db = new DatabaseHelper(getContext(), INTERNAL_DATABASE_NAME, true); 2001 } else if (EXTERNAL_VOLUME.equals(volume)) { 2002 String path = Environment.getExternalStorageDirectory().getPath(); 2003 int volumeID = FileUtils.getFatVolumeId(path); 2004 if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID); 2005 2006 // generate database name based on volume ID 2007 String dbName = "external-" + Integer.toHexString(volumeID) + ".db"; 2008 db = new DatabaseHelper(getContext(), dbName, false); 2009 } else { 2010 throw new IllegalArgumentException("There is no volume named " + volume); 2011 } 2012 2013 mDatabases.put(volume, db); 2014 2015 if (!db.mInternal) { 2016 // clean up stray album art files: delete every file not in the database 2017 File[] files = new File( 2018 Environment.getExternalStorageDirectory(), 2019 ALBUM_THUMB_FOLDER).listFiles(); 2020 HashSet<String> fileSet = new HashSet(); 2021 for (int i = 0; files != null && i < files.length; i++) { 2022 fileSet.add(files[i].getPath()); 2023 } 2024 2025 Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, 2026 new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null); 2027 try { 2028 while (cursor != null && cursor.moveToNext()) { 2029 fileSet.remove(cursor.getString(0)); 2030 } 2031 } finally { 2032 if (cursor != null) cursor.close(); 2033 } 2034 2035 Iterator<String> iterator = fileSet.iterator(); 2036 while (iterator.hasNext()) { 2037 String filename = iterator.next(); 2038 if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename); 2039 new File(filename).delete(); 2040 } 2041 } 2042 } 2043 2044 if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume); 2045 return Uri.parse("content://media/" + volume); 2046 } 2047 2048 /** 2049 * Detach the database for a volume (must be external). 2050 * Does nothing if the volume is already detached, otherwise 2051 * closes the database and sends a notification to listeners. 2052 * 2053 * @param uri The content URI of the volume, as returned by {@link #attachVolume} 2054 */ 2055 private void detachVolume(Uri uri) { 2056 if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) { 2057 throw new SecurityException( 2058 "Opening and closing databases not allowed."); 2059 } 2060 2061 String volume = uri.getPathSegments().get(0); 2062 if (INTERNAL_VOLUME.equals(volume)) { 2063 throw new UnsupportedOperationException( 2064 "Deleting the internal volume is not allowed"); 2065 } else if (!EXTERNAL_VOLUME.equals(volume)) { 2066 throw new IllegalArgumentException( 2067 "There is no volume named " + volume); 2068 } 2069 2070 synchronized (mDatabases) { 2071 DatabaseHelper database = mDatabases.get(volume); 2072 if (database == null) return; 2073 2074 try { 2075 // touch the database file to show it is most recently used 2076 File file = new File(database.getReadableDatabase().getPath()); 2077 file.setLastModified(System.currentTimeMillis()); 2078 } catch (SQLException e) { 2079 Log.e(TAG, "Can't touch database file", e); 2080 } 2081 2082 mDatabases.remove(volume); 2083 database.close(); 2084 } 2085 2086 getContext().getContentResolver().notifyChange(uri, null); 2087 if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume); 2088 } 2089 2090 private static String TAG = "MediaProvider"; 2091 private static final boolean LOCAL_LOGV = true; 2092 private static final int DATABASE_VERSION = 71; 2093 private static final String INTERNAL_DATABASE_NAME = "internal.db"; 2094 2095 // maximum number of cached external databases to keep 2096 private static final int MAX_EXTERNAL_DATABASES = 3; 2097 2098 // Delete databases that have not been used in two months 2099 // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60) 2100 private static final long OBSOLETE_DATABASE_DB = 5184000000L; 2101 2102 private HashMap<String, DatabaseHelper> mDatabases; 2103 2104 private Worker mThumbWorker; 2105 private Handler mThumbHandler; 2106 2107 // name of the volume currently being scanned by the media scanner (or null) 2108 private String mMediaScannerVolume; 2109 2110 static final String INTERNAL_VOLUME = "internal"; 2111 static final String EXTERNAL_VOLUME = "external"; 2112 static final String ALBUM_THUMB_FOLDER = "albumthumbs"; 2113 2114 // path for writing contents of in memory temp database 2115 private String mTempDatabasePath; 2116 2117 private static final int IMAGES_MEDIA = 1; 2118 private static final int IMAGES_MEDIA_ID = 2; 2119 private static final int IMAGES_THUMBNAILS = 3; 2120 private static final int IMAGES_THUMBNAILS_ID = 4; 2121 2122 private static final int AUDIO_MEDIA = 100; 2123 private static final int AUDIO_MEDIA_ID = 101; 2124 private static final int AUDIO_MEDIA_ID_GENRES = 102; 2125 private static final int AUDIO_MEDIA_ID_GENRES_ID = 103; 2126 private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104; 2127 private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105; 2128 private static final int AUDIO_GENRES = 106; 2129 private static final int AUDIO_GENRES_ID = 107; 2130 private static final int AUDIO_GENRES_ID_MEMBERS = 108; 2131 private static final int AUDIO_GENRES_ID_MEMBERS_ID = 109; 2132 private static final int AUDIO_PLAYLISTS = 110; 2133 private static final int AUDIO_PLAYLISTS_ID = 111; 2134 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 2135 private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113; 2136 private static final int AUDIO_ARTISTS = 114; 2137 private static final int AUDIO_ARTISTS_ID = 115; 2138 private static final int AUDIO_ALBUMS = 116; 2139 private static final int AUDIO_ALBUMS_ID = 117; 2140 private static final int AUDIO_ARTISTS_ID_ALBUMS = 118; 2141 private static final int AUDIO_ALBUMART = 119; 2142 private static final int AUDIO_ALBUMART_ID = 120; 2143 2144 private static final int VIDEO_MEDIA = 200; 2145 private static final int VIDEO_MEDIA_ID = 201; 2146 2147 private static final int VOLUMES = 300; 2148 private static final int VOLUMES_ID = 301; 2149 2150 private static final int AUDIO_SEARCH = 400; 2151 2152 private static final int MEDIA_SCANNER = 500; 2153 2154 private static final UriMatcher URI_MATCHER = 2155 new UriMatcher(UriMatcher.NO_MATCH); 2156 2157 private static final String[] MIME_TYPE_PROJECTION = new String[] { 2158 MediaStore.MediaColumns._ID, // 0 2159 MediaStore.MediaColumns.MIME_TYPE, // 1 2160 }; 2161 2162 private static final String[] EXTERNAL_DATABASE_TABLES = new String[] { 2163 "images", 2164 "thumbnails", 2165 "audio_meta", 2166 "artists", 2167 "albums", 2168 "audio_genres", 2169 "audio_genres_map", 2170 "audio_playlists", 2171 "audio_playlists_map", 2172 "video", 2173 }; 2174 2175 static 2176 { 2177 URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA); 2178 URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID); 2179 URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS); 2180 URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID); 2181 2182 URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA); 2183 URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID); 2184 URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES); 2185 URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID); 2186 URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS); 2187 URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID); 2188 URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES); 2189 URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID); 2190 URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS); 2191 URI_MATCHER.addURI("media", "*/audio/genres/#/members/#", AUDIO_GENRES_ID_MEMBERS_ID); 2192 URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS); 2193 URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID); 2194 URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS); 2195 URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID); 2196 URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS); 2197 URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID); 2198 URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS); 2199 URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS); 2200 URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID); 2201 URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART); 2202 URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID); 2203 2204 URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA); 2205 URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID); 2206 2207 URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER); 2208 2209 URI_MATCHER.addURI("media", "*", VOLUMES_ID); 2210 URI_MATCHER.addURI("media", null, VOLUMES); 2211 2212 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY, 2213 AUDIO_SEARCH); 2214 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 2215 AUDIO_SEARCH); 2216 } 2217} 2218