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