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