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