MediaProvider.java revision e3cc725934758f4ef732b5c7d1507b653071a412
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 537 /** 538 * Iterate through a table in a database, ensuring that the bucked id and names are correct. 539 * @param db 540 * @param tableName 541 */ 542 private static void updateBucketNames(SQLiteDatabase db, String tableName) { 543 // Rebuild the bucket_display_name column using the natural case rather than lower case. 544 db.beginTransaction(); 545 try { 546 String[] columns = {BaseColumns._ID, MediaColumns.DATA}; 547 Cursor cursor = db.query(tableName, columns, null, null, null, null, null); 548 try { 549 final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID); 550 final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA); 551 while (cursor.moveToNext()) { 552 String data = cursor.getString(dataColumnIndex); 553 ContentValues values = new ContentValues(); 554 computeBucketValues(data, values); 555 int rowId = cursor.getInt(idColumnIndex); 556 db.update(tableName, values, "_id=" + rowId, null); 557 } 558 } finally { 559 cursor.close(); 560 } 561 db.setTransactionSuccessful(); 562 } finally { 563 db.endTransaction(); 564 } 565 } 566 567 /** 568 * @param data The input path 569 * @param values the content values, where the bucked id name and display name are updated. 570 * 571 */ 572 573 private static void computeBucketValues(String data, ContentValues values) { 574 File parentFile = new File(data).getParentFile(); 575 if (parentFile == null) { 576 parentFile = new File("/"); 577 } 578 579 // Lowercase the path for hashing. This avoids duplicate buckets if the 580 // filepath case is changed externally. 581 // Keep the original case for display. 582 String path = parentFile.toString().toLowerCase(); 583 String name = parentFile.getName(); 584 585 // Note: the BUCKET_ID and BUCKET_DISPLAY_NAME attributes are spelled the 586 // same for both images and video. However, for backwards-compatibility reasons 587 // there is no common base class. We use the ImageColumns version here 588 values.put(ImageColumns.BUCKET_ID, path.hashCode()); 589 values.put(ImageColumns.BUCKET_DISPLAY_NAME, name); 590 } 591 592 @Override 593 public Cursor query(Uri uri, String[] projectionIn, String selection, 594 String[] selectionArgs, String sort) { 595 int table = URI_MATCHER.match(uri); 596 597 // handle MEDIA_SCANNER before calling getDatabaseForUri() 598 if (table == MEDIA_SCANNER) { 599 if (mMediaScannerVolume == null) { 600 return null; 601 } else { 602 // create a cursor to return volume currently being scanned by the media scanner 603 return new MediaScannerCursor(mMediaScannerVolume); 604 } 605 } 606 607 String groupBy = null; 608 DatabaseHelper database = getDatabaseForUri(uri); 609 if (database == null) { 610 return null; 611 } 612 SQLiteDatabase db = database.getReadableDatabase(); 613 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 614 615 switch (table) { 616 case IMAGES_MEDIA: 617 qb.setTables("images"); 618 if (uri.getQueryParameter("distinct") != null) 619 qb.setDistinct(true); 620 621 // set the project map so that data dir is prepended to _data. 622 //qb.setProjectionMap(mImagesProjectionMap, true); 623 break; 624 625 case IMAGES_MEDIA_ID: 626 qb.setTables("images"); 627 if (uri.getQueryParameter("distinct") != null) 628 qb.setDistinct(true); 629 630 // set the project map so that data dir is prepended to _data. 631 //qb.setProjectionMap(mImagesProjectionMap, true); 632 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 633 break; 634 635 case IMAGES_THUMBNAILS: 636 qb.setTables("thumbnails"); 637 break; 638 639 case IMAGES_THUMBNAILS_ID: 640 qb.setTables("thumbnails"); 641 qb.appendWhere("_id = " + uri.getPathSegments().get(3)); 642 break; 643 644 case AUDIO_MEDIA: 645 qb.setTables("audio "); 646 break; 647 648 case AUDIO_MEDIA_ID: 649 qb.setTables("audio"); 650 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 651 break; 652 653 case AUDIO_MEDIA_ID_GENRES: 654 qb.setTables("audio_genres"); 655 qb.appendWhere("_id IN (SELECT genre_id FROM " + 656 "audio_genres_map WHERE audio_id = " + 657 uri.getPathSegments().get(3) + ")"); 658 break; 659 660 case AUDIO_MEDIA_ID_GENRES_ID: 661 qb.setTables("audio_genres"); 662 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 663 break; 664 665 case AUDIO_MEDIA_ID_PLAYLISTS: 666 qb.setTables("audio_playlists"); 667 qb.appendWhere("_id IN (SELECT playlist_id FROM " + 668 "audio_playlists_map WHERE audio_id = " + 669 uri.getPathSegments().get(3) + ")"); 670 break; 671 672 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 673 qb.setTables("audio_playlists"); 674 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 675 break; 676 677 case AUDIO_GENRES: 678 qb.setTables("audio_genres"); 679 break; 680 681 case AUDIO_GENRES_ID: 682 qb.setTables("audio_genres"); 683 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 684 break; 685 686 case AUDIO_GENRES_ID_MEMBERS: 687 qb.setTables("audio"); 688 qb.appendWhere("_id IN (SELECT audio_id FROM " + 689 "audio_genres_map WHERE genre_id = " + 690 uri.getPathSegments().get(3) + ")"); 691 break; 692 693 case AUDIO_GENRES_ID_MEMBERS_ID: 694 qb.setTables("audio"); 695 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 696 break; 697 698 case AUDIO_PLAYLISTS: 699 qb.setTables("audio_playlists"); 700 break; 701 702 case AUDIO_PLAYLISTS_ID: 703 qb.setTables("audio_playlists"); 704 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 705 break; 706 707 case AUDIO_PLAYLISTS_ID_MEMBERS: 708 for (int i = 0; i < projectionIn.length; i++) { 709 if (projectionIn[i].equals("_id")) { 710 projectionIn[i] = "audio_playlists_map._id AS _id"; 711 } 712 } 713 qb.setTables("audio_playlists_map, audio"); 714 qb.appendWhere("audio._id = audio_id AND playlist_id = " 715 + uri.getPathSegments().get(3)); 716 break; 717 718 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 719 qb.setTables("audio"); 720 qb.appendWhere("_id=" + uri.getPathSegments().get(5)); 721 break; 722 723 case VIDEO_MEDIA: 724 qb.setTables("video"); 725 break; 726 727 case VIDEO_MEDIA_ID: 728 qb.setTables("video"); 729 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 730 break; 731 732 case AUDIO_ARTISTS: 733 qb.setTables("artist_info"); 734 break; 735 736 case AUDIO_ARTISTS_ID: 737 qb.setTables("artist_info"); 738 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 739 break; 740 741 case AUDIO_ARTISTS_ID_ALBUMS: 742 String aid = uri.getPathSegments().get(3); 743 qb.setTables("audio LEFT OUTER JOIN album_art ON" + 744 " audio.album_id=album_art.album_id"); 745 qb.appendWhere("is_music=1 AND audio.album_id IN (SELECT album_id FROM " + 746 "artists_albums_map WHERE artist_id = " + 747 aid + ")"); 748 groupBy = "audio.album_id"; 749 sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 750 "count(CASE WHEN artist_id==" + aid + " THEN 'foo' ELSE NULL END) AS " + 751 MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 752 qb.setProjectionMap(sArtistAlbumsMap); 753 break; 754 755 case AUDIO_ALBUMS: 756 qb.setTables("album_info"); 757 break; 758 759 case AUDIO_ALBUMS_ID: 760 qb.setTables("album_info"); 761 qb.appendWhere("_id=" + uri.getPathSegments().get(3)); 762 break; 763 764 case AUDIO_ALBUMART_ID: 765 qb.setTables("album_art"); 766 qb.appendWhere("album_id=" + uri.getPathSegments().get(3)); 767 break; 768 769 case AUDIO_SEARCH: 770 return doAudioSearch(db, qb, uri, projectionIn, selection, selectionArgs, sort); 771 772 default: 773 throw new IllegalStateException("Unknown URL: " + uri.toString()); 774 } 775 776 Cursor c = qb.query(db, projectionIn, selection, 777 selectionArgs, groupBy, null, sort); 778 if (c != null) { 779 c.setNotificationUri(getContext().getContentResolver(), uri); 780 } 781 return c; 782 } 783 784 private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb, 785 Uri uri, String[] projectionIn, String selection, 786 String[] selectionArgs, String sort) { 787 788 List<String> l = uri.getPathSegments(); 789 String mSearchString = l.size() == 4 ? l.get(3) : ""; 790 mSearchString = mSearchString.replaceAll(" ", " ").trim(); 791 Cursor mCursor = null; 792 793 String [] searchWords = mSearchString.length() > 0 ? 794 mSearchString.split(" ") : new String[0]; 795 String [] wildcardWords3 = new String[searchWords.length * 3]; 796 Collator col = Collator.getInstance(); 797 col.setStrength(Collator.PRIMARY); 798 int len = searchWords.length; 799 for (int i = 0; i < len; i++) { 800 wildcardWords3[i] = wildcardWords3[i + len] = wildcardWords3[i + len + len] = 801 '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%'; 802 } 803 804 String UQs [] = new String[3]; 805 HashSet<String> tablecolumns = new HashSet<String>(); 806 807 // Direct match artists 808 { 809 String[] ccols = new String[] { 810 MediaStore.Audio.Artists._ID, 811 "'artist' AS " + MediaStore.Audio.Media.MIME_TYPE, 812 "" + R.drawable.ic_search_category_music_artist + " AS " + 813 SearchManager.SUGGEST_COLUMN_ICON_1, 814 "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2, 815 MediaStore.Audio.Artists.ARTIST + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 816 MediaStore.Audio.Artists.NUMBER_OF_ALBUMS + " AS data1", 817 MediaStore.Audio.Artists.NUMBER_OF_TRACKS + " AS data2", 818 MediaStore.Audio.Artists.ARTIST_KEY + " AS ar", // 819 "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 820 "'content://media/external/audio/artists/'||" + MediaStore.Audio.Artists._ID + 821 " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA, 822 "'1' AS grouporder", 823 "artist_key AS itemorder" 824 }; 825 826 827 String where = MediaStore.Audio.Artists.ARTIST_KEY + " != ''"; 828 for (int i = 0; i < searchWords.length; i++) { 829 where += " AND ar LIKE ?"; 830 } 831 832 qb.setTables("artist_info"); 833 UQs[0] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE, 834 ccols, tablecolumns, 12, "artist", where, null, null, null); 835 } 836 837 // Direct match albums 838 { 839 String[] ccols = new String[] { 840 MediaStore.Audio.Albums._ID, 841 "'album' AS " + MediaStore.Audio.Media.MIME_TYPE, 842 "" + R.drawable.ic_search_category_music_album + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1, 843 "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2, 844 MediaStore.Audio.Albums.ALBUM + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 845 MediaStore.Audio.Media.ARTIST + " AS data1", 846 "null AS data2", 847 MediaStore.Audio.Media.ARTIST_KEY + 848 "||' '||" + 849 MediaStore.Audio.Media.ALBUM_KEY + 850 " AS ar_al", 851 "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 852 "'content://media/external/audio/albums/'||" + MediaStore.Audio.Albums._ID + 853 " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA, 854 "'2' AS grouporder", 855 "album_key AS itemorder" 856 }; 857 858 String where = MediaStore.Audio.Media.ALBUM_KEY + " != ''"; 859 for (int i = 0; i < searchWords.length; i++) { 860 where += " AND ar_al LIKE ?"; 861 } 862 863 qb = new SQLiteQueryBuilder(); 864 qb.setTables("album_info"); 865 UQs[1] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE, 866 ccols, tablecolumns, 12, "album", where, null, null, null); 867 } 868 869 // Direct match tracks 870 { 871 String[] ccols = new String[] { 872 "audio._id AS _id", 873 MediaStore.Audio.Media.MIME_TYPE, 874 "" + R.drawable.ic_search_category_music_song + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1, 875 "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2, 876 MediaStore.Audio.Media.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, 877 MediaStore.Audio.Media.ARTIST + " AS data1", 878 MediaStore.Audio.Media.ALBUM + " AS data2", 879 MediaStore.Audio.Media.ARTIST_KEY + 880 "||' '||" + 881 MediaStore.Audio.Media.ALBUM_KEY + 882 "||' '||" + 883 MediaStore.Audio.Media.TITLE_KEY + 884 " AS ar_al_ti", 885 "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 886 "'content://media/external/audio/media/'||audio._id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA, 887 "'3' AS grouporder", 888 "title_key AS itemorder" 889 }; 890 891 String where = MediaStore.Audio.Media.TITLE + " != ''"; 892 893 for (int i = 0; i < searchWords.length; i++) { 894 where += " AND ar_al_ti LIKE ?"; 895 } 896 qb = new SQLiteQueryBuilder(); 897 qb.setTables("audio"); 898 UQs[2] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE, 899 ccols, tablecolumns, 12, "audio/", where, null, null, null); 900 } 901 902 if (mCursor != null) { 903 mCursor.deactivate(); 904 mCursor = null; 905 } 906 if (UQs[0] != null && UQs[1] != null && UQs[2] != null) { 907 String union = qb.buildUnionQuery(UQs, "grouporder,itemorder", null); 908 mCursor = db.rawQuery(union, wildcardWords3); 909 } 910 911 return mCursor; 912 } 913 914 @Override 915 public String getType(Uri url) 916 { 917 switch (URI_MATCHER.match(url)) { 918 case IMAGES_MEDIA_ID: 919 case AUDIO_MEDIA_ID: 920 case AUDIO_GENRES_ID_MEMBERS_ID: 921 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 922 case VIDEO_MEDIA_ID: 923 Cursor c = query(url, MIME_TYPE_PROJECTION, null, null, null); 924 if (c != null && c.getCount() == 1) { 925 c.moveToFirst(); 926 String mimeType = c.getString(1); 927 c.deactivate(); 928 return mimeType; 929 } 930 break; 931 932 case IMAGES_MEDIA: 933 case IMAGES_THUMBNAILS: 934 return Images.Media.CONTENT_TYPE; 935 case IMAGES_THUMBNAILS_ID: 936 return "image/jpeg"; 937 938 case AUDIO_MEDIA: 939 case AUDIO_GENRES_ID_MEMBERS: 940 case AUDIO_PLAYLISTS_ID_MEMBERS: 941 return Audio.Media.CONTENT_TYPE; 942 943 case AUDIO_GENRES: 944 case AUDIO_MEDIA_ID_GENRES: 945 return Audio.Genres.CONTENT_TYPE; 946 case AUDIO_GENRES_ID: 947 case AUDIO_MEDIA_ID_GENRES_ID: 948 return Audio.Genres.ENTRY_CONTENT_TYPE; 949 case AUDIO_PLAYLISTS: 950 case AUDIO_MEDIA_ID_PLAYLISTS: 951 return Audio.Playlists.CONTENT_TYPE; 952 case AUDIO_PLAYLISTS_ID: 953 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 954 return Audio.Playlists.ENTRY_CONTENT_TYPE; 955 956 case VIDEO_MEDIA: 957 return Video.Media.CONTENT_TYPE; 958 } 959 throw new IllegalStateException("Unknown URL"); 960 } 961 962 /** 963 * Ensures there is a file in the _data column of values, if one isn't 964 * present a new file is created. 965 * 966 * @param initialValues the values passed to insert by the caller 967 * @return the new values 968 */ 969 private ContentValues ensureFile(boolean internal, ContentValues initialValues, 970 String preferredExtension, String directoryName) { 971 ContentValues values; 972 String file = initialValues.getAsString("_data"); 973 if (TextUtils.isEmpty(file)) { 974 file = generateFileName(internal, preferredExtension, directoryName); 975 values = new ContentValues(initialValues); 976 values.put("_data", file); 977 } else { 978 values = initialValues; 979 } 980 981 if (!ensureFileExists(file)) { 982 throw new IllegalStateException("Unable to create new file: " + file); 983 } 984 return values; 985 } 986 987 @Override 988 public int bulkInsert(Uri uri, ContentValues values[]) { 989 int match = URI_MATCHER.match(uri); 990 if (match == VOLUMES) { 991 return super.bulkInsert(uri, values); 992 } 993 DatabaseHelper database = getDatabaseForUri(uri); 994 if (database == null) { 995 throw new UnsupportedOperationException( 996 "Unknown URI: " + uri); 997 } 998 SQLiteDatabase db = database.getWritableDatabase(); 999 db.beginTransaction(); 1000 int numInserted = 0; 1001 try { 1002 int len = values.length; 1003 for (int i = 0; i < len; i++) { 1004 insertInternal(uri, values[i]); 1005 } 1006 numInserted = len; 1007 db.setTransactionSuccessful(); 1008 } finally { 1009 db.endTransaction(); 1010 } 1011 getContext().getContentResolver().notifyChange(uri, null); 1012 return numInserted; 1013 } 1014 1015 @Override 1016 public Uri insert(Uri uri, ContentValues initialValues) 1017 { 1018 Uri newUri = insertInternal(uri, initialValues); 1019 if (newUri != null) { 1020 getContext().getContentResolver().notifyChange(uri, null); 1021 } 1022 1023 return newUri; 1024 } 1025 1026 private Uri insertInternal(Uri uri, ContentValues initialValues) { 1027 long rowId; 1028 int match = URI_MATCHER.match(uri); 1029 1030 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1031 if (match == MEDIA_SCANNER) { 1032 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 1033 return MediaStore.getMediaScannerUri(); 1034 } 1035 1036 Uri newUri = null; 1037 DatabaseHelper database = getDatabaseForUri(uri); 1038 if (database == null && match != VOLUMES) { 1039 throw new UnsupportedOperationException( 1040 "Unknown URI: " + uri); 1041 } 1042 SQLiteDatabase db = (match == VOLUMES ? null : database.getWritableDatabase()); 1043 1044 if (initialValues == null) { 1045 initialValues = new ContentValues(); 1046 } 1047 1048 switch (match) { 1049 case IMAGES_MEDIA: { 1050 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/Camera"); 1051 1052 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1053 computeBucketValues(values.getAsString(MediaColumns.DATA), values); 1054 rowId = db.insert("images", "name", values); 1055 1056 if (rowId > 0) { 1057 newUri = ContentUris.withAppendedId( 1058 Images.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 1059 } 1060 break; 1061 } 1062 1063 case IMAGES_THUMBNAILS: { 1064 ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/.thumbnails"); 1065 rowId = db.insert("thumbnails", "name", values); 1066 if (rowId > 0) { 1067 newUri = ContentUris.withAppendedId(Images.Thumbnails. 1068 getContentUri(uri.getPathSegments().get(0)), rowId); 1069 } 1070 break; 1071 } 1072 1073 case AUDIO_MEDIA: { 1074 // SQLite Views are read-only, so we need to deconstruct this 1075 // insert and do inserts into the underlying tables. 1076 // If doing this here turns out to be a performance bottleneck, 1077 // consider moving this to native code and using triggers on 1078 // the view. 1079 ContentValues values = new ContentValues(initialValues); 1080 1081 // Insert the artist into the artist table and remove it from 1082 // the input values 1083 Object so = values.get("artist"); 1084 String s = (so == null ? "" : so.toString()); 1085 values.remove("artist"); 1086 long artistRowId; 1087 HashMap<String, Long> artistCache = database.mArtistCache; 1088 synchronized(artistCache) { 1089 Long temp = artistCache.get(s); 1090 if (temp == null) { 1091 artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist", 1092 s, null, artistCache, uri); 1093 } else { 1094 artistRowId = temp.longValue(); 1095 } 1096 } 1097 1098 // Do the same for the album field 1099 so = values.get("album"); 1100 s = (so == null ? "" : so.toString()); 1101 values.remove("album"); 1102 long albumRowId; 1103 HashMap<String, Long> albumCache = database.mAlbumCache; 1104 synchronized(albumCache) { 1105 Long temp = albumCache.get(s); 1106 if (temp == null) { 1107 String path = values.getAsString("_data"); 1108 albumRowId = getKeyIdForName(db, "albums", "album_key", "album", 1109 s, path, albumCache, uri); 1110 } else { 1111 albumRowId = temp; 1112 } 1113 } 1114 1115 values.put("artist_id", Integer.toString((int)artistRowId)); 1116 values.put("album_id", Integer.toString((int)albumRowId)); 1117 so = values.getAsString("title"); 1118 s = (so == null ? "" : so.toString()); 1119 values.put("title_key", MediaStore.Audio.keyFor(s)); 1120 1121 so = values.getAsString("_data"); 1122 s = (so == null ? "" : so.toString()); 1123 int idx = s.lastIndexOf('/'); 1124 if (idx >= 0) { 1125 s = s.substring(idx + 1); 1126 } 1127 values.put("_display_name", s); 1128 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1129 1130 rowId = db.insert("audio_meta", "duration", values); 1131 if (rowId > 0) { 1132 newUri = ContentUris.withAppendedId(Audio.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 1133 } 1134 break; 1135 } 1136 1137 case AUDIO_MEDIA_ID_GENRES: { 1138 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 1139 ContentValues values = new ContentValues(initialValues); 1140 values.put(Audio.Genres.Members.AUDIO_ID, audioId); 1141 rowId = db.insert("audio_playlists_map", "genre_id", values); 1142 if (rowId > 0) { 1143 newUri = ContentUris.withAppendedId(uri, rowId); 1144 } 1145 break; 1146 } 1147 1148 case AUDIO_MEDIA_ID_PLAYLISTS: { 1149 Long audioId = Long.parseLong(uri.getPathSegments().get(2)); 1150 ContentValues values = new ContentValues(initialValues); 1151 values.put(Audio.Playlists.Members.AUDIO_ID, audioId); 1152 rowId = db.insert("audio_playlists_map", "playlist_id", 1153 values); 1154 if (rowId > 0) { 1155 newUri = ContentUris.withAppendedId(uri, rowId); 1156 } 1157 break; 1158 } 1159 1160 case AUDIO_GENRES: { 1161 rowId = db.insert("audio_genres", "audio_id", initialValues); 1162 if (rowId > 0) { 1163 newUri = ContentUris.withAppendedId(Audio.Genres.getContentUri(uri.getPathSegments().get(0)), rowId); 1164 } 1165 break; 1166 } 1167 1168 case AUDIO_GENRES_ID_MEMBERS: { 1169 Long genreId = Long.parseLong(uri.getPathSegments().get(3)); 1170 ContentValues values = new ContentValues(initialValues); 1171 values.put(Audio.Genres.Members.GENRE_ID, genreId); 1172 rowId = db.insert("audio_genres_map", "genre_id", values); 1173 if (rowId > 0) { 1174 newUri = ContentUris.withAppendedId(uri, rowId); 1175 } 1176 break; 1177 } 1178 1179 case AUDIO_PLAYLISTS: { 1180 ContentValues values = new ContentValues(initialValues); 1181 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 1182 rowId = db.insert("audio_playlists", "name", initialValues); 1183 if (rowId > 0) { 1184 newUri = ContentUris.withAppendedId(Audio.Playlists.getContentUri(uri.getPathSegments().get(0)), rowId); 1185 } 1186 break; 1187 } 1188 1189 case AUDIO_PLAYLISTS_ID: 1190 case AUDIO_PLAYLISTS_ID_MEMBERS: { 1191 Long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 1192 ContentValues values = new ContentValues(initialValues); 1193 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId); 1194 rowId = db.insert("audio_playlists_map", "playlist_id", 1195 values); 1196 if (rowId > 0) { 1197 newUri = ContentUris.withAppendedId(uri, rowId); 1198 } 1199 break; 1200 } 1201 1202 case VIDEO_MEDIA: { 1203 ContentValues values = ensureFile(database.mInternal, initialValues, ".3gp", "video"); 1204 String so = values.getAsString("_data"); 1205 String nonNullSo = (so == null ? "" : so.toString()); 1206 String s = nonNullSo; 1207 int idx = s.lastIndexOf('/'); 1208 if (idx >= 0) { 1209 s = s.substring(idx + 1); 1210 } 1211 values.put("_display_name", s); 1212 computeBucketValues(so, values); 1213 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 1214 rowId = db.insert("video", "artist", values); 1215 if (rowId > 0) { 1216 newUri = ContentUris.withAppendedId(Video.Media.getContentUri(uri.getPathSegments().get(0)), rowId); 1217 } 1218 break; 1219 } 1220 1221 case AUDIO_ALBUMART: 1222 if (database.mInternal) { 1223 throw new UnsupportedOperationException("no internal album art allowed"); 1224 } 1225 ContentValues values = null; 1226 try { 1227 values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 1228 } catch (IllegalStateException ex) { 1229 // probably no more room to store albumthumbs 1230 values = initialValues; 1231 } 1232 rowId = db.insert("album_art", "_data", values); 1233 if (rowId > 0) { 1234 newUri = ContentUris.withAppendedId(uri, rowId); 1235 } 1236 break; 1237 1238 case VOLUMES: 1239 return attachVolume(initialValues.getAsString("name")); 1240 1241 default: 1242 throw new UnsupportedOperationException("Invalid URI " + uri); 1243 } 1244 1245 return newUri; 1246 } 1247 1248 private String generateFileName(boolean internal, String preferredExtension, String directoryName) 1249 { 1250 // create a random file 1251 String name = String.valueOf(System.currentTimeMillis()); 1252 1253 if (internal) { 1254 throw new UnsupportedOperationException("Writing to internal storage is not supported."); 1255// return Environment.getDataDirectory() 1256// + "/" + directoryName + "/" + name + preferredExtension; 1257 } else { 1258 return Environment.getExternalStorageDirectory() 1259 + "/" + directoryName + "/" + name + preferredExtension; 1260 } 1261 } 1262 1263 private boolean ensureFileExists(String path) { 1264 File file = new File(path); 1265 if (file.exists()) { 1266 return true; 1267 } else { 1268 // we will not attempt to create the first directory in the path 1269 // (for example, do not create /sdcard if the SD card is not mounted) 1270 int secondSlash = path.indexOf('/', 1); 1271 if (secondSlash < 1) return false; 1272 String directoryPath = path.substring(0, secondSlash); 1273 File directory = new File(directoryPath); 1274 if (!directory.exists()) 1275 return false; 1276 file.getParentFile().mkdirs(); 1277 try { 1278 return file.createNewFile(); 1279 } catch(IOException ioe) { 1280 Log.e(TAG, "File creation failed", ioe); 1281 } 1282 return false; 1283 } 1284 } 1285 1286 private static final class GetTableAndWhereOutParameter { 1287 public String table; 1288 public String where; 1289 } 1290 1291 static final GetTableAndWhereOutParameter sGetTableAndWhereParam = 1292 new GetTableAndWhereOutParameter(); 1293 1294 private void getTableAndWhere(Uri uri, int match, String userWhere, 1295 GetTableAndWhereOutParameter out) { 1296 String where = null; 1297 switch (match) { 1298 case IMAGES_MEDIA_ID: 1299 out.table = "images"; 1300 where = "_id = " + uri.getPathSegments().get(3); 1301 break; 1302 1303 case AUDIO_MEDIA: 1304 out.table = "audio"; 1305 break; 1306 1307 case AUDIO_MEDIA_ID: 1308 out.table = "audio"; 1309 where = "_id=" + uri.getPathSegments().get(3); 1310 break; 1311 1312 case AUDIO_MEDIA_ID_GENRES: 1313 out.table = "audio_genres"; 1314 where = "audio_id=" + uri.getPathSegments().get(3); 1315 break; 1316 1317 case AUDIO_MEDIA_ID_GENRES_ID: 1318 out.table = "audio_genres"; 1319 where = "audio_id=" + uri.getPathSegments().get(3) + 1320 " AND genre_id=" + uri.getPathSegments().get(5); 1321 break; 1322 1323 case AUDIO_MEDIA_ID_PLAYLISTS: 1324 out.table = "audio_playlists"; 1325 where = "audio_id=" + uri.getPathSegments().get(3); 1326 break; 1327 1328 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1329 out.table = "audio_playlists"; 1330 where = "audio_id=" + uri.getPathSegments().get(3) + 1331 " AND playlists_id=" + uri.getPathSegments().get(5); 1332 break; 1333 1334 case AUDIO_GENRES: 1335 out.table = "audio_genres"; 1336 break; 1337 1338 case AUDIO_GENRES_ID: 1339 out.table = "audio_genres"; 1340 where = "_id=" + uri.getPathSegments().get(3); 1341 break; 1342 1343 case AUDIO_GENRES_ID_MEMBERS: 1344 out.table = "audio_genres"; 1345 where = "genre_id=" + uri.getPathSegments().get(3); 1346 break; 1347 1348 case AUDIO_GENRES_ID_MEMBERS_ID: 1349 out.table = "audio_genres"; 1350 where = "genre_id=" + uri.getPathSegments().get(3) + 1351 " AND audio_id =" + uri.getPathSegments().get(5); 1352 break; 1353 1354 case AUDIO_PLAYLISTS: 1355 out.table = "audio_playlists"; 1356 break; 1357 1358 case AUDIO_PLAYLISTS_ID: 1359 out.table = "audio_playlists"; 1360 where = "_id=" + uri.getPathSegments().get(3); 1361 break; 1362 1363 case AUDIO_PLAYLISTS_ID_MEMBERS: 1364 out.table = "audio_playlists_map"; 1365 where = "playlist_id=" + uri.getPathSegments().get(3); 1366 break; 1367 1368 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1369 out.table = "audio_playlists_map"; 1370 where = "playlist_id=" + uri.getPathSegments().get(3) + 1371 " AND _id=" + uri.getPathSegments().get(5); 1372 break; 1373 1374 case AUDIO_ALBUMART_ID: 1375 out.table = "album_art"; 1376 where = "album_id=" + uri.getPathSegments().get(3); 1377 break; 1378 1379 case VIDEO_MEDIA: 1380 out.table = "video"; 1381 break; 1382 1383 case VIDEO_MEDIA_ID: 1384 out.table = "video"; 1385 where = "_id=" + uri.getPathSegments().get(3); 1386 break; 1387 1388 default: 1389 throw new UnsupportedOperationException( 1390 "Unknown or unsupported URL: " + uri.toString()); 1391 } 1392 1393 // Add in the user requested WHERE clause, if needed 1394 if (!TextUtils.isEmpty(userWhere)) { 1395 if (!TextUtils.isEmpty(where)) { 1396 out.where = where + " AND (" + userWhere + ")"; 1397 } else { 1398 out.where = userWhere; 1399 } 1400 } else { 1401 out.where = where; 1402 } 1403 } 1404 1405 @Override 1406 public int delete(Uri uri, String userWhere, String[] whereArgs) { 1407 int count; 1408 int match = URI_MATCHER.match(uri); 1409 1410 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1411 if (match == MEDIA_SCANNER) { 1412 if (mMediaScannerVolume == null) { 1413 return 0; 1414 } 1415 mMediaScannerVolume = null; 1416 return 1; 1417 } 1418 1419 if (match != VOLUMES_ID) { 1420 DatabaseHelper database = getDatabaseForUri(uri); 1421 if (database == null) { 1422 throw new UnsupportedOperationException( 1423 "Unknown URI: " + uri); 1424 } 1425 SQLiteDatabase db = database.getWritableDatabase(); 1426 1427 synchronized (sGetTableAndWhereParam) { 1428 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 1429 switch (match) { 1430 case AUDIO_MEDIA: 1431 case AUDIO_MEDIA_ID: 1432 count = db.delete("audio_meta", 1433 sGetTableAndWhereParam.where, whereArgs); 1434 break; 1435 default: 1436 count = db.delete(sGetTableAndWhereParam.table, 1437 sGetTableAndWhereParam.where, whereArgs); 1438 break; 1439 } 1440 getContext().getContentResolver().notifyChange(uri, null); 1441 } 1442 } else { 1443 detachVolume(uri); 1444 count = 1; 1445 } 1446 1447 return count; 1448 } 1449 1450 @Override 1451 public int update(Uri uri, ContentValues initialValues, String userWhere, 1452 String[] whereArgs) { 1453 int count; 1454 int match = URI_MATCHER.match(uri); 1455 1456 DatabaseHelper database = getDatabaseForUri(uri); 1457 if (database == null) { 1458 throw new UnsupportedOperationException( 1459 "Unknown URI: " + uri); 1460 } 1461 SQLiteDatabase db = database.getWritableDatabase(); 1462 1463 synchronized (sGetTableAndWhereParam) { 1464 getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam); 1465 1466 switch (match) { 1467 case AUDIO_MEDIA: 1468 case AUDIO_MEDIA_ID: 1469 { 1470 ContentValues values = new ContentValues(initialValues); 1471 // Insert the artist into the artist table and remove it from 1472 // the input values 1473 String so = values.getAsString("artist"); 1474 if (so != null) { 1475 String s = so.toString(); 1476 values.remove("artist"); 1477 long artistRowId; 1478 HashMap<String, Long> artistCache = database.mArtistCache; 1479 synchronized(artistCache) { 1480 Long temp = artistCache.get(s); 1481 if (temp == null) { 1482 artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist", 1483 s, null, artistCache, uri); 1484 } else { 1485 artistRowId = temp.longValue(); 1486 } 1487 } 1488 values.put("artist_id", Integer.toString((int)artistRowId)); 1489 } 1490 1491 // Do the same for the album field 1492 so = values.getAsString("album"); 1493 if (so != null) { 1494 String s = so.toString(); 1495 values.remove("album"); 1496 long albumRowId; 1497 HashMap<String, Long> albumCache = database.mAlbumCache; 1498 synchronized(albumCache) { 1499 Long temp = albumCache.get(s); 1500 if (temp == null) { 1501 albumRowId = getKeyIdForName(db, "albums", "album_key", "album", 1502 s, null, albumCache, uri); 1503 } else { 1504 albumRowId = temp.longValue(); 1505 } 1506 } 1507 values.put("album_id", Integer.toString((int)albumRowId)); 1508 } 1509 1510 // don't allow the title_key field to be updated directly 1511 values.remove("title_key"); 1512 // If the title field is modified, update the title_key 1513 so = values.getAsString("title"); 1514 if (so != null) { 1515 String s = so.toString(); 1516 values.put("title_key", MediaStore.Audio.keyFor(s)); 1517 } 1518 1519 count = db.update("audio_meta", values, sGetTableAndWhereParam.where, 1520 whereArgs); 1521 } 1522 break; 1523 case IMAGES_MEDIA: 1524 case IMAGES_MEDIA_ID: 1525 case VIDEO_MEDIA: 1526 case VIDEO_MEDIA_ID: 1527 { 1528 ContentValues values = new ContentValues(initialValues); 1529 // Don't allow bucket id or display name to be updated directly. 1530 // The same names are used for both images and table columns, so 1531 // we use the ImageColumns constants here. 1532 values.remove(ImageColumns.BUCKET_ID); 1533 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 1534 // If the data is being modified update the bucket values 1535 String data = values.getAsString(MediaColumns.DATA); 1536 if (data != null) { 1537 computeBucketValues(data, values); 1538 } 1539 count = db.update(sGetTableAndWhereParam.table, values, 1540 sGetTableAndWhereParam.where, whereArgs); 1541 } 1542 break; 1543 default: 1544 count = db.update(sGetTableAndWhereParam.table, initialValues, 1545 sGetTableAndWhereParam.where, whereArgs); 1546 break; 1547 } 1548 } 1549 if (count > 0) { 1550 getContext().getContentResolver().notifyChange(uri, null); 1551 } 1552 return count; 1553 } 1554 1555 private static final String[] openFileColumns = new String[] { 1556 MediaStore.MediaColumns.DATA, 1557 }; 1558 1559 @Override 1560 public ParcelFileDescriptor openFile(Uri uri, String mode) 1561 throws FileNotFoundException { 1562 ParcelFileDescriptor pfd = null; 1563 try { 1564 pfd = openFileHelper(uri, mode); 1565 } catch (FileNotFoundException ex) { 1566 if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_ID) { 1567 // Tried to open an album art file which does not exist. Regenerate. 1568 DatabaseHelper database = getDatabaseForUri(uri); 1569 if (database == null) { 1570 throw ex; 1571 } 1572 SQLiteDatabase db = database.getReadableDatabase(); 1573 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1574 int albumid = Integer.parseInt(uri.getPathSegments().get(3)); 1575 qb.setTables("audio"); 1576 qb.appendWhere("album_id=" + albumid); 1577 Cursor c = qb.query(db, 1578 new String [] { 1579 MediaStore.Audio.Media.DATA }, 1580 null, null, null, null, null); 1581 c.moveToFirst(); 1582 if (!c.isAfterLast()) { 1583 String audiopath = c.getString(0); 1584 makeThumb(db, audiopath, albumid, uri); 1585 } 1586 c.close(); 1587 } 1588 throw ex; 1589 } 1590 return pfd; 1591 } 1592 1593 private class Worker implements Runnable { 1594 private final Object mLock = new Object(); 1595 private Looper mLooper; 1596 1597 Worker(String name) { 1598 Thread t = new Thread(null, this, name); 1599 t.setPriority(Thread.MIN_PRIORITY); 1600 t.start(); 1601 synchronized (mLock) { 1602 while (mLooper == null) { 1603 try { 1604 mLock.wait(); 1605 } catch (InterruptedException ex) { 1606 } 1607 } 1608 } 1609 } 1610 1611 public Looper getLooper() { 1612 return mLooper; 1613 } 1614 1615 public void run() { 1616 synchronized (mLock) { 1617 Looper.prepare(); 1618 mLooper = Looper.myLooper(); 1619 mLock.notifyAll(); 1620 } 1621 Looper.loop(); 1622 } 1623 1624 public void quit() { 1625 mLooper.quit(); 1626 } 1627 } 1628 1629 private class ThumbData { 1630 SQLiteDatabase db; 1631 String path; 1632 long album_id; 1633 Uri albumart_uri; 1634 } 1635 1636 private void makeThumb(SQLiteDatabase db, String path, long album_id, 1637 Uri albumart_uri) { 1638 ThumbData d = new ThumbData(); 1639 d.db = db; 1640 d.path = path; 1641 d.album_id = album_id; 1642 d.albumart_uri = albumart_uri; 1643 Message msg = mThumbHandler.obtainMessage(); 1644 msg.obj = d; 1645 msg.sendToTarget(); 1646 } 1647 1648 private void makeThumb(ThumbData d) { 1649 SQLiteDatabase db = d.db; 1650 String path = d.path; 1651 long album_id = d.album_id; 1652 Uri albumart_uri = d.albumart_uri; 1653 1654 try { 1655 File f = new File(path); 1656 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, 1657 ParcelFileDescriptor.MODE_READ_ONLY); 1658 1659 MediaScanner scanner = new MediaScanner(getContext()); 1660 byte [] art = scanner.extractAlbumArt(pfd.getFileDescriptor()); 1661 pfd.close(); 1662 1663 // if no embedded art exists, look for AlbumArt.jpg in same directory as the media file 1664 if (art == null && path != null) { 1665 int lastSlash = path.lastIndexOf('/'); 1666 if (lastSlash > 0) { 1667 String artPath = path.substring(0, lastSlash + 1) + "AlbumArt.jpg"; 1668 File file = new File(artPath); 1669 if (file.exists()) { 1670 art = new byte[(int)file.length()]; 1671 FileInputStream stream = null; 1672 try { 1673 stream = new FileInputStream(file); 1674 stream.read(art); 1675 } catch (IOException ex) { 1676 art = null; 1677 } finally { 1678 if (stream != null) { 1679 stream.close(); 1680 } 1681 } 1682 } 1683 } 1684 } 1685 1686 Bitmap bm = null; 1687 if (art != null) { 1688 try { 1689 // get the size of the bitmap 1690 BitmapFactory.Options opts = new BitmapFactory.Options(); 1691 opts.inJustDecodeBounds = true; 1692 opts.inSampleSize = 1; 1693 BitmapFactory.decodeByteArray(art, 0, art.length, opts); 1694 1695 // request a reasonably sized output image 1696 // TODO: don't hardcode the size 1697 while (opts.outHeight > 320 || opts.outWidth > 320) { 1698 opts.outHeight /= 2; 1699 opts.outWidth /= 2; 1700 opts.inSampleSize *= 2; 1701 } 1702 1703 // get the image for real now 1704 opts.inJustDecodeBounds = false; 1705 opts.inPreferredConfig = Bitmap.Config.RGB_565; 1706 bm = BitmapFactory.decodeByteArray(art, 0, art.length, opts); 1707 } catch (Exception e) { 1708 } 1709 } 1710 if (bm != null && bm.getConfig() == null) { 1711 bm = bm.copy(Bitmap.Config.RGB_565, false); 1712 } 1713 if (bm != null) { 1714 // save bitmap 1715 Uri out = null; 1716 // TODO: this could be done more efficiently with a call to db.replace(), which 1717 // replaces or inserts as needed, making it unnecessary to query() first. 1718 if (albumart_uri != null) { 1719 Cursor c = query(albumart_uri, new String [] { "_data" }, 1720 null, null, null); 1721 c.moveToFirst(); 1722 if (!c.isAfterLast()) { 1723 String albumart_path = c.getString(0); 1724 if (ensureFileExists(albumart_path)) { 1725 out = albumart_uri; 1726 } 1727 } 1728 c.close(); 1729 } else { 1730 ContentValues initialValues = new ContentValues(); 1731 initialValues.put("album_id", album_id); 1732 try { 1733 ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER); 1734 long rowId = db.insert("album_art", "_data", values); 1735 if (rowId > 0) { 1736 out = ContentUris.withAppendedId(ALBUMART_URI, rowId); 1737 } 1738 } catch (IllegalStateException ex) { 1739 Log.e(TAG, "error creating album thumb file"); 1740 } 1741 } 1742 if (out != null) { 1743 boolean success = false; 1744 try { 1745 OutputStream outstream = getContext().getContentResolver().openOutputStream(out); 1746 success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream); 1747 outstream.close(); 1748 } catch (FileNotFoundException ex) { 1749 Log.e(TAG, "error creating file", ex); 1750 } catch (IOException ex) { 1751 Log.e(TAG, "error creating file", ex); 1752 } 1753 if (!success) { 1754 // the thumbnail was not written successfully, delete the entry that refers to it 1755 getContext().getContentResolver().delete(out, null, null); 1756 } 1757 } 1758 getContext().getContentResolver().notifyChange(MEDIA_URI, null); 1759 } 1760 } catch (IOException ex) { 1761 } 1762 1763 } 1764 1765 /** 1766 * Look up the artist or album entry for the given name, creating that entry 1767 * if it does not already exists. 1768 * @param db The database 1769 * @param table The table to store the key/name pair in. 1770 * @param keyField The name of the key-column 1771 * @param nameField The name of the name-column 1772 * @param rawName The name that the calling app was trying to insert into the database 1773 * @param path The path to the file being inserted in to the audio table 1774 * @param cache The cache to add this entry to 1775 * @param srcuri The Uri that prompted the call to this method, used for determining whether this is 1776 * the internal or external database 1777 * @return The row ID for this artist/album, or -1 if the provided name was invalid 1778 */ 1779 private long getKeyIdForName(SQLiteDatabase db, String table, String keyField, String nameField, 1780 String rawName, String path, HashMap<String, Long> cache, Uri srcuri) { 1781 long rowId; 1782 1783 if (rawName == null || rawName.length() == 0) { 1784 return -1; 1785 } 1786 String k = MediaStore.Audio.keyFor(rawName); 1787 1788 if (k == null) { 1789 return -1; 1790 } 1791 1792 String [] selargs = { k }; 1793 Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null); 1794 1795 try { 1796 switch (c.getCount()) { 1797 case 0: { 1798 // insert new entry into table 1799 ContentValues otherValues = new ContentValues(); 1800 otherValues.put(keyField, k); 1801 otherValues.put(nameField, rawName); 1802 rowId = db.insert(table, "duration", otherValues); 1803 if (path != null && table.equals("albums") && 1804 ! rawName.equals(MediaFile.UNKNOWN_STRING)) { 1805 // We just inserted a new album. Now create an album art thumbnail for it. 1806 makeThumb(db, path, rowId, null); 1807 } 1808 if (rowId > 0) { 1809 String volume = srcuri.toString().substring(16, 24); // extract internal/external 1810 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 1811 getContext().getContentResolver().notifyChange(uri, null); 1812 } 1813 } 1814 break; 1815 case 1: { 1816 // Use the existing entry 1817 c.moveToFirst(); 1818 rowId = c.getLong(0); 1819 1820 // Determine whether the current rawName is better than what's 1821 // currently stored in the table, and update the table if it is. 1822 String currentFancyName = c.getString(2); 1823 String bestName = makeBestName(rawName, currentFancyName); 1824 if (!bestName.equals(currentFancyName)) { 1825 // update the table with the new name 1826 ContentValues newValues = new ContentValues(); 1827 newValues.put(nameField, bestName); 1828 db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null); 1829 String volume = srcuri.toString().substring(16, 24); // extract internal/external 1830 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 1831 getContext().getContentResolver().notifyChange(uri, null); 1832 } 1833 } 1834 break; 1835 default: 1836 // corrupt database 1837 Log.e(TAG, "Multiple entries in table " + table + " for key " + k); 1838 rowId = -1; 1839 break; 1840 } 1841 } finally { 1842 if (c != null) c.close(); 1843 } 1844 1845 if (cache != null && ! rawName.equals(MediaFile.UNKNOWN_STRING)) { 1846 cache.put(rawName, rowId); 1847 } 1848 return rowId; 1849 } 1850 1851 /** 1852 * Returns the best string to use for display, given two names. 1853 * Note that this function does not necessarily return either one 1854 * of the provided names; it may decide to return a better alternative 1855 * (for example, specifying the inputs "Police" and "Police, The" will 1856 * return "The Police") 1857 * 1858 * The basic assumptions are: 1859 * - longer is better ("The police" is better than "Police") 1860 * - prefix is better ("The Police" is better than "Police, The") 1861 * - accents are better ("Motörhead" is better than "Motorhead") 1862 * 1863 * @param one The first of the two names to consider 1864 * @param two The last of the two names to consider 1865 * @return The actual name to use 1866 */ 1867 String makeBestName(String one, String two) { 1868 String name; 1869 1870 // Longer names are usually better. 1871 if (one.length() > two.length()) { 1872 name = one; 1873 } else { 1874 // Names with accents are usually better, and conveniently sort later 1875 if (one.toLowerCase().compareTo(two.toLowerCase()) > 0) { 1876 name = one; 1877 } else { 1878 name = two; 1879 } 1880 } 1881 1882 // Prefixes are better than postfixes. 1883 if (name.endsWith(", the") || name.endsWith(",the") || 1884 name.endsWith(", an") || name.endsWith(",an") || 1885 name.endsWith(", a") || name.endsWith(",a")) { 1886 String fix = name.substring(1 + name.lastIndexOf(',')); 1887 name = fix.trim() + " " + name.substring(0, name.lastIndexOf(',')); 1888 } 1889 1890 // TODO: word-capitalize the resulting name 1891 return name; 1892 } 1893 1894 1895 /** 1896 * Looks up the database based on the given URI. 1897 * 1898 * @param uri The requested URI 1899 * @returns the database for the given URI 1900 */ 1901 private DatabaseHelper getDatabaseForUri(Uri uri) { 1902 synchronized (mDatabases) { 1903 if (uri.getPathSegments().size() > 1) { 1904 return mDatabases.get(uri.getPathSegments().get(0)); 1905 } 1906 } 1907 return null; 1908 } 1909 1910 /** 1911 * Attach the database for a volume (internal or external). 1912 * Does nothing if the volume is already attached, otherwise 1913 * checks the volume ID and sets up the corresponding database. 1914 * 1915 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}. 1916 * @return the content URI of the attached volume. 1917 */ 1918 private Uri attachVolume(String volume) { 1919 if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) { 1920 throw new SecurityException( 1921 "Opening and closing databases not allowed."); 1922 } 1923 1924 synchronized (mDatabases) { 1925 if (mDatabases.get(volume) != null) { // Already attached 1926 return Uri.parse("content://media/" + volume); 1927 } 1928 1929 DatabaseHelper db; 1930 if (INTERNAL_VOLUME.equals(volume)) { 1931 db = new DatabaseHelper(getContext(), INTERNAL_DATABASE_NAME, true); 1932 } else if (EXTERNAL_VOLUME.equals(volume)) { 1933 String path = Environment.getExternalStorageDirectory().getPath(); 1934 int volumeID = FileUtils.getFatVolumeId(path); 1935 if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID); 1936 1937 // generate database name based on volume ID 1938 String dbName = "external-" + Integer.toHexString(volumeID) + ".db"; 1939 db = new DatabaseHelper(getContext(), dbName, false); 1940 } else { 1941 throw new IllegalArgumentException("There is no volume named " + volume); 1942 } 1943 1944 mDatabases.put(volume, db); 1945 1946 if (!db.mInternal) { 1947 // clean up stray album art files: delete every file not in the database 1948 File[] files = new File( 1949 Environment.getExternalStorageDirectory(), 1950 ALBUM_THUMB_FOLDER).listFiles(); 1951 HashSet<String> fileSet = new HashSet(); 1952 for (int i = 0; files != null && i < files.length; i++) { 1953 fileSet.add(files[i].getPath()); 1954 } 1955 1956 Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, 1957 new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null); 1958 try { 1959 while (cursor != null && cursor.moveToNext()) { 1960 fileSet.remove(cursor.getString(0)); 1961 } 1962 } finally { 1963 if (cursor != null) cursor.close(); 1964 } 1965 1966 Iterator<String> iterator = fileSet.iterator(); 1967 while (iterator.hasNext()) { 1968 String filename = iterator.next(); 1969 if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename); 1970 new File(filename).delete(); 1971 } 1972 } 1973 } 1974 1975 if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume); 1976 return Uri.parse("content://media/" + volume); 1977 } 1978 1979 /** 1980 * Detach the database for a volume (must be external). 1981 * Does nothing if the volume is already detached, otherwise 1982 * closes the database and sends a notification to listeners. 1983 * 1984 * @param uri The content URI of the volume, as returned by {@link #attachVolume} 1985 */ 1986 private void detachVolume(Uri uri) { 1987 if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) { 1988 throw new SecurityException( 1989 "Opening and closing databases not allowed."); 1990 } 1991 1992 String volume = uri.getPathSegments().get(0); 1993 if (INTERNAL_VOLUME.equals(volume)) { 1994 throw new UnsupportedOperationException( 1995 "Deleting the internal volume is not allowed"); 1996 } else if (!EXTERNAL_VOLUME.equals(volume)) { 1997 throw new IllegalArgumentException( 1998 "There is no volume named " + volume); 1999 } 2000 2001 synchronized (mDatabases) { 2002 DatabaseHelper database = mDatabases.get(volume); 2003 if (database == null) return; 2004 2005 try { 2006 // touch the database file to show it is most recently used 2007 File file = new File(database.getReadableDatabase().getPath()); 2008 file.setLastModified(System.currentTimeMillis()); 2009 } catch (SQLException e) { 2010 Log.e(TAG, "Can't touch database file", e); 2011 } 2012 2013 mDatabases.remove(volume); 2014 database.close(); 2015 } 2016 2017 getContext().getContentResolver().notifyChange(uri, null); 2018 if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume); 2019 } 2020 2021 private static String TAG = "MediaProvider"; 2022 private static final boolean LOCAL_LOGV = true; 2023 private static final int DATABASE_VERSION = 68; 2024 private static final String INTERNAL_DATABASE_NAME = "internal.db"; 2025 2026 // maximum number of cached external databases to keep 2027 private static final int MAX_EXTERNAL_DATABASES = 3; 2028 2029 // Delete databases that have not been used in two months 2030 // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60) 2031 private static final long OBSOLETE_DATABASE_DB = 5184000000L; 2032 2033 private HashMap<String, DatabaseHelper> mDatabases; 2034 2035 private Worker mThumbWorker; 2036 private Handler mThumbHandler; 2037 2038 // name of the volume currently being scanned by the media scanner (or null) 2039 private String mMediaScannerVolume; 2040 2041 static final String INTERNAL_VOLUME = "internal"; 2042 static final String EXTERNAL_VOLUME = "external"; 2043 static final String ALBUM_THUMB_FOLDER = "albumthumbs"; 2044 2045 // path for writing contents of in memory temp database 2046 private String mTempDatabasePath; 2047 2048 private static final int IMAGES_MEDIA = 1; 2049 private static final int IMAGES_MEDIA_ID = 2; 2050 private static final int IMAGES_THUMBNAILS = 3; 2051 private static final int IMAGES_THUMBNAILS_ID = 4; 2052 2053 private static final int AUDIO_MEDIA = 100; 2054 private static final int AUDIO_MEDIA_ID = 101; 2055 private static final int AUDIO_MEDIA_ID_GENRES = 102; 2056 private static final int AUDIO_MEDIA_ID_GENRES_ID = 103; 2057 private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104; 2058 private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105; 2059 private static final int AUDIO_GENRES = 106; 2060 private static final int AUDIO_GENRES_ID = 107; 2061 private static final int AUDIO_GENRES_ID_MEMBERS = 108; 2062 private static final int AUDIO_GENRES_ID_MEMBERS_ID = 109; 2063 private static final int AUDIO_PLAYLISTS = 110; 2064 private static final int AUDIO_PLAYLISTS_ID = 111; 2065 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 2066 private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113; 2067 private static final int AUDIO_ARTISTS = 114; 2068 private static final int AUDIO_ARTISTS_ID = 115; 2069 private static final int AUDIO_ALBUMS = 116; 2070 private static final int AUDIO_ALBUMS_ID = 117; 2071 private static final int AUDIO_ARTISTS_ID_ALBUMS = 118; 2072 private static final int AUDIO_ALBUMART = 119; 2073 private static final int AUDIO_ALBUMART_ID = 120; 2074 2075 private static final int VIDEO_MEDIA = 200; 2076 private static final int VIDEO_MEDIA_ID = 201; 2077 2078 private static final int VOLUMES = 300; 2079 private static final int VOLUMES_ID = 301; 2080 2081 private static final int AUDIO_SEARCH = 400; 2082 2083 private static final int MEDIA_SCANNER = 500; 2084 2085 private static final UriMatcher URI_MATCHER = 2086 new UriMatcher(UriMatcher.NO_MATCH); 2087 2088 private static final String[] MIME_TYPE_PROJECTION = new String[] { 2089 MediaStore.MediaColumns._ID, // 0 2090 MediaStore.MediaColumns.MIME_TYPE, // 1 2091 }; 2092 2093 private static final String[] EXTERNAL_DATABASE_TABLES = new String[] { 2094 "images", 2095 "thumbnails", 2096 "audio_meta", 2097 "artists", 2098 "albums", 2099 "audio_genres", 2100 "audio_genres_map", 2101 "audio_playlists", 2102 "audio_playlists_map", 2103 "video", 2104 }; 2105 2106 static 2107 { 2108 URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA); 2109 URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID); 2110 URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS); 2111 URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID); 2112 2113 URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA); 2114 URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID); 2115 URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES); 2116 URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID); 2117 URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS); 2118 URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID); 2119 URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES); 2120 URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID); 2121 URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS); 2122 URI_MATCHER.addURI("media", "*/audio/genres/#/members/#", AUDIO_GENRES_ID_MEMBERS_ID); 2123 URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS); 2124 URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID); 2125 URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS); 2126 URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID); 2127 URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS); 2128 URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID); 2129 URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS); 2130 URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS); 2131 URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID); 2132 URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART); 2133 URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID); 2134 2135 URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA); 2136 URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID); 2137 2138 URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER); 2139 2140 URI_MATCHER.addURI("media", "*", VOLUMES_ID); 2141 URI_MATCHER.addURI("media", null, VOLUMES); 2142 2143 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY, 2144 AUDIO_SEARCH); 2145 URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 2146 AUDIO_SEARCH); 2147 } 2148} 2149