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