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