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