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