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