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