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