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