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