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