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