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