MediaProvider.java revision fb598dd5ac235f6282aac23b7b9c214f2fd62a7a
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        // In version 89, originally we updateBucketNames(db, "images") and
1131        // updateBucketNames(db, "video"), but in version 101 we now updateBucketNames
1132        //  for all files and therefore can save the update here.
1133
1134        if (fromVersion < 91) {
1135            // Never query by mini_thumb_magic_index
1136            db.execSQL("DROP INDEX IF EXISTS mini_thumb_magic_index");
1137
1138            // sort the items by taken date in each bucket
1139            db.execSQL("CREATE INDEX IF NOT EXISTS image_bucket_index ON images(bucket_id, datetaken)");
1140            db.execSQL("CREATE INDEX IF NOT EXISTS video_bucket_index ON video(bucket_id, datetaken)");
1141        }
1142
1143        // versions 92 - 98 were work in progress on MTP obsoleted by version 99
1144        if (fromVersion < 92) {
1145            // Delete albums and artists, then clear the modification time on songs, which
1146            // will cause the media scanner to rescan everything, rebuilding the artist and
1147            // album tables along the way, while preserving playlists.
1148            // We need this rescan because ICU also changed, and now generates different
1149            // collation keys
1150            db.execSQL("DELETE from albums");
1151            db.execSQL("DELETE from artists");
1152            db.execSQL("UPDATE audio_meta SET date_modified=0;");
1153        }
1154
1155        if (fromVersion < 99) {
1156            // Remove various stages of work in progress for MTP support
1157            db.execSQL("DROP TABLE IF EXISTS objects");
1158            db.execSQL("DROP TABLE IF EXISTS files");
1159            db.execSQL("DROP TRIGGER IF EXISTS images_objects_cleanup;");
1160            db.execSQL("DROP TRIGGER IF EXISTS audio_objects_cleanup;");
1161            db.execSQL("DROP TRIGGER IF EXISTS video_objects_cleanup;");
1162            db.execSQL("DROP TRIGGER IF EXISTS playlists_objects_cleanup;");
1163            db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_images;");
1164            db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_audio;");
1165            db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_video;");
1166            db.execSQL("DROP TRIGGER IF EXISTS files_cleanup_playlists;");
1167            db.execSQL("DROP TRIGGER IF EXISTS media_cleanup;");
1168
1169            // Create a new table to manage all files in our storage.
1170            // This contains a union of all the columns from the old
1171            // images, audio_meta, videos and audio_playlist tables.
1172            db.execSQL("CREATE TABLE files (" +
1173                        "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1174                        "_data TEXT," +     // this can be null for playlists
1175                        "_size INTEGER," +
1176                        "format INTEGER," +
1177                        "parent INTEGER," +
1178                        "date_added INTEGER," +
1179                        "date_modified INTEGER," +
1180                        "mime_type TEXT," +
1181                        "title TEXT," +
1182                        "description TEXT," +
1183                        "_display_name TEXT," +
1184
1185                        // for images
1186                        "picasa_id TEXT," +
1187                        "orientation INTEGER," +
1188
1189                        // for images and video
1190                        "latitude DOUBLE," +
1191                        "longitude DOUBLE," +
1192                        "datetaken INTEGER," +
1193                        "mini_thumb_magic INTEGER," +
1194                        "bucket_id TEXT," +
1195                        "bucket_display_name TEXT," +
1196                        "isprivate INTEGER," +
1197
1198                        // for audio
1199                        "title_key TEXT," +
1200                        "artist_id INTEGER," +
1201                        "album_id INTEGER," +
1202                        "composer TEXT," +
1203                        "track INTEGER," +
1204                        "year INTEGER CHECK(year!=0)," +
1205                        "is_ringtone INTEGER," +
1206                        "is_music INTEGER," +
1207                        "is_alarm INTEGER," +
1208                        "is_notification INTEGER," +
1209                        "is_podcast INTEGER," +
1210
1211                        // for audio and video
1212                        "duration INTEGER," +
1213                        "bookmark INTEGER," +
1214
1215                        // for video
1216                        "artist TEXT," +
1217                        "album TEXT," +
1218                        "resolution TEXT," +
1219                        "tags TEXT," +
1220                        "category TEXT," +
1221                        "language TEXT," +
1222                        "mini_thumb_data TEXT," +
1223
1224                        // for playlists
1225                        "name TEXT," +
1226
1227                        // media_type is used by the views to emulate the old
1228                        // images, audio_meta, videos and audio_playlist tables.
1229                        "media_type INTEGER," +
1230
1231                        // Value of _id from the old media table.
1232                        // Used only for updating other tables during database upgrade.
1233                        "old_id INTEGER" +
1234                       ");");
1235            db.execSQL("CREATE INDEX path_index ON files(_data);");
1236            db.execSQL("CREATE INDEX media_type_index ON files(media_type);");
1237
1238            // Copy all data from our obsolete tables to the new files table
1239            db.execSQL("INSERT INTO files (" + IMAGE_COLUMNS + ",old_id,media_type) SELECT "
1240                    + IMAGE_COLUMNS + ",_id," + FileColumns.MEDIA_TYPE_IMAGE + " FROM images;");
1241            db.execSQL("INSERT INTO files (" + AUDIO_COLUMNSv99 + ",old_id,media_type) SELECT "
1242                    + AUDIO_COLUMNSv99 + ",_id," + FileColumns.MEDIA_TYPE_AUDIO + " FROM audio_meta;");
1243            db.execSQL("INSERT INTO files (" + VIDEO_COLUMNS + ",old_id,media_type) SELECT "
1244                    + VIDEO_COLUMNS + ",_id," + FileColumns.MEDIA_TYPE_VIDEO + " FROM video;");
1245            if (!internal) {
1246                db.execSQL("INSERT INTO files (" + PLAYLIST_COLUMNS + ",old_id,media_type) SELECT "
1247                        + PLAYLIST_COLUMNS + ",_id," + FileColumns.MEDIA_TYPE_PLAYLIST
1248                        + " FROM audio_playlists;");
1249            }
1250
1251            // Delete the old tables
1252            db.execSQL("DROP TABLE IF EXISTS images");
1253            db.execSQL("DROP TABLE IF EXISTS audio_meta");
1254            db.execSQL("DROP TABLE IF EXISTS video");
1255            db.execSQL("DROP TABLE IF EXISTS audio_playlists");
1256
1257            // Create views to replace our old tables
1258            db.execSQL("CREATE VIEW images AS SELECT _id," + IMAGE_COLUMNS +
1259                        " FROM files WHERE " + FileColumns.MEDIA_TYPE + "="
1260                        + FileColumns.MEDIA_TYPE_IMAGE + ";");
1261// audio_meta will be created below for schema 100
1262//            db.execSQL("CREATE VIEW audio_meta AS SELECT _id," + AUDIO_COLUMNSv99 +
1263//                        " FROM files WHERE " + FileColumns.MEDIA_TYPE + "="
1264//                        + FileColumns.MEDIA_TYPE_AUDIO + ";");
1265            db.execSQL("CREATE VIEW video AS SELECT _id," + VIDEO_COLUMNS +
1266                        " FROM files WHERE " + FileColumns.MEDIA_TYPE + "="
1267                        + FileColumns.MEDIA_TYPE_VIDEO + ";");
1268            if (!internal) {
1269                db.execSQL("CREATE VIEW audio_playlists AS SELECT _id," + PLAYLIST_COLUMNS +
1270                        " FROM files WHERE " + FileColumns.MEDIA_TYPE + "="
1271                        + FileColumns.MEDIA_TYPE_PLAYLIST + ";");
1272            }
1273
1274            // update the image_id column in the thumbnails table.
1275            db.execSQL("UPDATE thumbnails SET image_id = (SELECT _id FROM files "
1276                        + "WHERE files.old_id = thumbnails.image_id AND files.media_type = "
1277                        + FileColumns.MEDIA_TYPE_IMAGE + ");");
1278
1279            if (!internal) {
1280                // update audio_id in the audio_genres_map and audio_playlists_map tables.
1281                db.execSQL("UPDATE audio_genres_map SET audio_id = (SELECT _id FROM files "
1282                        + "WHERE files.old_id = audio_genres_map.audio_id AND files.media_type = "
1283                        + FileColumns.MEDIA_TYPE_AUDIO + ");");
1284                db.execSQL("UPDATE audio_playlists_map SET audio_id = (SELECT _id FROM files "
1285                        + "WHERE files.old_id = audio_playlists_map.audio_id "
1286                        + "AND files.media_type = " + FileColumns.MEDIA_TYPE_AUDIO + ");");
1287            }
1288
1289            // update video_id in the videothumbnails table.
1290            db.execSQL("UPDATE videothumbnails SET video_id = (SELECT _id FROM files "
1291                        + "WHERE files.old_id = videothumbnails.video_id AND files.media_type = "
1292                        + FileColumns.MEDIA_TYPE_VIDEO + ");");
1293
1294            // update indices to work on the files table
1295            db.execSQL("DROP INDEX IF EXISTS title_idx");
1296            db.execSQL("DROP INDEX IF EXISTS album_id_idx");
1297            db.execSQL("DROP INDEX IF EXISTS image_bucket_index");
1298            db.execSQL("DROP INDEX IF EXISTS video_bucket_index");
1299            db.execSQL("DROP INDEX IF EXISTS sort_index");
1300            db.execSQL("DROP INDEX IF EXISTS titlekey_index");
1301            db.execSQL("DROP INDEX IF EXISTS artist_id_idx");
1302            db.execSQL("CREATE INDEX title_idx ON files(title);");
1303            db.execSQL("CREATE INDEX album_id_idx ON files(album_id);");
1304            db.execSQL("CREATE INDEX bucket_index ON files(bucket_id, datetaken);");
1305            db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC);");
1306            db.execSQL("CREATE INDEX titlekey_index ON files(title_key);");
1307            db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id);");
1308
1309            // Recreate triggers for our obsolete tables on the new files table
1310            db.execSQL("DROP TRIGGER IF EXISTS images_cleanup");
1311            db.execSQL("DROP TRIGGER IF EXISTS audio_meta_cleanup");
1312            db.execSQL("DROP TRIGGER IF EXISTS video_cleanup");
1313            db.execSQL("DROP TRIGGER IF EXISTS audio_playlists_cleanup");
1314            db.execSQL("DROP TRIGGER IF EXISTS audio_delete");
1315
1316            db.execSQL("CREATE TRIGGER IF NOT EXISTS images_cleanup DELETE ON files " +
1317                    "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_IMAGE + " " +
1318                    "BEGIN " +
1319                        "DELETE FROM thumbnails WHERE image_id = old._id;" +
1320                        "SELECT _DELETE_FILE(old._data);" +
1321                    "END");
1322
1323            db.execSQL("CREATE TRIGGER IF NOT EXISTS video_cleanup DELETE ON files " +
1324                    "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_VIDEO + " " +
1325                    "BEGIN " +
1326                        "SELECT _DELETE_FILE(old._data);" +
1327                    "END");
1328
1329            if (!internal) {
1330                db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_meta_cleanup DELETE ON files " +
1331                       "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_AUDIO + " " +
1332                       "BEGIN " +
1333                           "DELETE FROM audio_genres_map WHERE audio_id = old._id;" +
1334                           "DELETE FROM audio_playlists_map WHERE audio_id = old._id;" +
1335                       "END");
1336
1337                db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_playlists_cleanup DELETE ON files " +
1338                       "WHEN old.media_type = " + FileColumns.MEDIA_TYPE_PLAYLIST + " " +
1339                       "BEGIN " +
1340                           "DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" +
1341                           "SELECT _DELETE_FILE(old._data);" +
1342                       "END");
1343
1344                db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_delete INSTEAD OF DELETE ON audio " +
1345                        "BEGIN " +
1346                            "DELETE from files where _id=old._id;" +
1347                            "DELETE from audio_playlists_map where audio_id=old._id;" +
1348                            "DELETE from audio_genres_map where audio_id=old._id;" +
1349                        "END");
1350            }
1351        }
1352
1353        if (fromVersion < 100) {
1354            db.execSQL("ALTER TABLE files ADD COLUMN album_artist TEXT;");
1355            db.execSQL("DROP VIEW IF EXISTS audio_meta;");
1356            db.execSQL("CREATE VIEW audio_meta AS SELECT _id," + AUDIO_COLUMNSv100 +
1357                    " FROM files WHERE " + FileColumns.MEDIA_TYPE + "="
1358                    + FileColumns.MEDIA_TYPE_AUDIO + ";");
1359            db.execSQL("UPDATE files SET date_modified=0 WHERE " + FileColumns.MEDIA_TYPE + "="
1360                    + FileColumns.MEDIA_TYPE_AUDIO + ";");
1361        }
1362
1363        if (fromVersion < 300) {
1364            // we now compute bucket and display names for all files to avoid problems with files
1365            // that the media scanner might not recognize as images or videos
1366            updateBucketNames(db, "files");
1367        }
1368
1369        sanityCheck(db, fromVersion);
1370    }
1371
1372    /**
1373     * Perform a simple sanity check on the database. Currently this tests
1374     * whether all the _data entries in audio_meta are unique
1375     */
1376    private static void sanityCheck(SQLiteDatabase db, int fromVersion) {
1377        Cursor c1 = db.query("audio_meta", new String[] {"count(*)"},
1378                null, null, null, null, null);
1379        Cursor c2 = db.query("audio_meta", new String[] {"count(distinct _data)"},
1380                null, null, null, null, null);
1381        c1.moveToFirst();
1382        c2.moveToFirst();
1383        int num1 = c1.getInt(0);
1384        int num2 = c2.getInt(0);
1385        c1.close();
1386        c2.close();
1387        if (num1 != num2) {
1388            Log.e(TAG, "audio_meta._data column is not unique while upgrading" +
1389                    " from schema " +fromVersion + " : " + num1 +"/" + num2);
1390            // Delete all audio_meta rows so they will be rebuilt by the media scanner
1391            db.execSQL("DELETE FROM audio_meta;");
1392        }
1393    }
1394
1395    private static void recreateAudioView(SQLiteDatabase db) {
1396        // Provides a unified audio/artist/album info view.
1397        // Note that views are read-only, so we define a trigger to allow deletes.
1398        db.execSQL("DROP VIEW IF EXISTS audio");
1399        db.execSQL("DROP TRIGGER IF EXISTS audio_delete");
1400        db.execSQL("CREATE VIEW IF NOT EXISTS audio as SELECT * FROM audio_meta " +
1401                    "LEFT OUTER JOIN artists ON audio_meta.artist_id=artists.artist_id " +
1402                    "LEFT OUTER JOIN albums ON audio_meta.album_id=albums.album_id;");
1403
1404        db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_delete INSTEAD OF DELETE ON audio " +
1405                "BEGIN " +
1406                    "DELETE from audio_meta where _id=old._id;" +
1407                    "DELETE from audio_playlists_map where audio_id=old._id;" +
1408                    "DELETE from audio_genres_map where audio_id=old._id;" +
1409                "END");
1410    }
1411
1412    /**
1413     * Iterate through the rows of a table in a database, ensuring that the bucket_id and
1414     * bucket_display_name columns are correct.
1415     * @param db
1416     * @param tableName
1417     */
1418    private static void updateBucketNames(SQLiteDatabase db, String tableName) {
1419        // Rebuild the bucket_display_name column using the natural case rather than lower case.
1420        db.beginTransaction();
1421        try {
1422            String[] columns = {BaseColumns._ID, MediaColumns.DATA};
1423            Cursor cursor = db.query(tableName, columns, null, null, null, null, null);
1424            try {
1425                final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID);
1426                final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA);
1427                while (cursor.moveToNext()) {
1428                    String data = cursor.getString(dataColumnIndex);
1429                    ContentValues values = new ContentValues();
1430                    computeBucketValues(data, values);
1431                    int rowId = cursor.getInt(idColumnIndex);
1432                    db.update(tableName, values, "_id=" + rowId, null);
1433                }
1434            } finally {
1435                cursor.close();
1436            }
1437            db.setTransactionSuccessful();
1438        } finally {
1439            db.endTransaction();
1440        }
1441    }
1442
1443    /**
1444     * Iterate through the rows of a table in a database, ensuring that the
1445     * display name column has a value.
1446     * @param db
1447     * @param tableName
1448     */
1449    private static void updateDisplayName(SQLiteDatabase db, String tableName) {
1450        // Fill in default values for null displayName values
1451        db.beginTransaction();
1452        try {
1453            String[] columns = {BaseColumns._ID, MediaColumns.DATA, MediaColumns.DISPLAY_NAME};
1454            Cursor cursor = db.query(tableName, columns, null, null, null, null, null);
1455            try {
1456                final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID);
1457                final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA);
1458                final int displayNameIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME);
1459                ContentValues values = new ContentValues();
1460                while (cursor.moveToNext()) {
1461                    String displayName = cursor.getString(displayNameIndex);
1462                    if (displayName == null) {
1463                        String data = cursor.getString(dataColumnIndex);
1464                        values.clear();
1465                        computeDisplayName(data, values);
1466                        int rowId = cursor.getInt(idColumnIndex);
1467                        db.update(tableName, values, "_id=" + rowId, null);
1468                    }
1469                }
1470            } finally {
1471                cursor.close();
1472            }
1473            db.setTransactionSuccessful();
1474        } finally {
1475            db.endTransaction();
1476        }
1477    }
1478    /**
1479     * @param data The input path
1480     * @param values the content values, where the bucked id name and bucket display name are updated.
1481     *
1482     */
1483
1484    private static void computeBucketValues(String data, ContentValues values) {
1485        data = mediaToExternalPath(data);
1486        File parentFile = new File(data).getParentFile();
1487        if (parentFile == null) {
1488            parentFile = new File("/");
1489        }
1490
1491        // Lowercase the path for hashing. This avoids duplicate buckets if the
1492        // filepath case is changed externally.
1493        // Keep the original case for display.
1494        String path = parentFile.toString().toLowerCase();
1495        String name = parentFile.getName();
1496
1497        // Note: the BUCKET_ID and BUCKET_DISPLAY_NAME attributes are spelled the
1498        // same for both images and video. However, for backwards-compatibility reasons
1499        // there is no common base class. We use the ImageColumns version here
1500        values.put(ImageColumns.BUCKET_ID, path.hashCode());
1501        values.put(ImageColumns.BUCKET_DISPLAY_NAME, name);
1502    }
1503
1504    /**
1505     * @param data The input path
1506     * @param values the content values, where the display name is updated.
1507     *
1508     */
1509    private static void computeDisplayName(String data, ContentValues values) {
1510        String s = (data == null ? "" : data.toString());
1511        int idx = s.lastIndexOf('/');
1512        if (idx >= 0) {
1513            s = s.substring(idx + 1);
1514        }
1515        values.put("_display_name", s);
1516    }
1517
1518    /**
1519     * Copy taken time from date_modified if we lost the original value (e.g. after factory reset)
1520     * This works for both video and image tables.
1521     *
1522     * @param values the content values, where taken time is updated.
1523     */
1524    private static void computeTakenTime(ContentValues values) {
1525        if (! values.containsKey(Images.Media.DATE_TAKEN)) {
1526            // This only happens when MediaScanner finds an image file that doesn't have any useful
1527            // reference to get this value. (e.g. GPSTimeStamp)
1528            Long lastModified = values.getAsLong(MediaColumns.DATE_MODIFIED);
1529            if (lastModified != null) {
1530                values.put(Images.Media.DATE_TAKEN, lastModified * 1000);
1531            }
1532        }
1533    }
1534
1535    /**
1536     * This method blocks until thumbnail is ready.
1537     *
1538     * @param thumbUri
1539     * @return
1540     */
1541    private boolean waitForThumbnailReady(Uri origUri) {
1542        Cursor c = this.query(origUri, new String[] { ImageColumns._ID, ImageColumns.DATA,
1543                ImageColumns.MINI_THUMB_MAGIC}, null, null, null);
1544        if (c == null) return false;
1545
1546        boolean result = false;
1547
1548        if (c.moveToFirst()) {
1549            long id = c.getLong(0);
1550            String path = c.getString(1);
1551            long magic = c.getLong(2);
1552            path = mediaToExternalPath(path);
1553
1554            MediaThumbRequest req = requestMediaThumbnail(path, origUri,
1555                    MediaThumbRequest.PRIORITY_HIGH, magic);
1556            if (req == null) {
1557                return false;
1558            }
1559            synchronized (req) {
1560                try {
1561                    while (req.mState == MediaThumbRequest.State.WAIT) {
1562                        req.wait();
1563                    }
1564                } catch (InterruptedException e) {
1565                    Log.w(TAG, e);
1566                }
1567                if (req.mState == MediaThumbRequest.State.DONE) {
1568                    result = true;
1569                }
1570            }
1571        }
1572        c.close();
1573
1574        return result;
1575    }
1576
1577    private boolean matchThumbRequest(MediaThumbRequest req, int pid, long id, long gid,
1578            boolean isVideo) {
1579        boolean cancelAllOrigId = (id == -1);
1580        boolean cancelAllGroupId = (gid == -1);
1581        return (req.mCallingPid == pid) &&
1582                (cancelAllGroupId || req.mGroupId == gid) &&
1583                (cancelAllOrigId || req.mOrigId == id) &&
1584                (req.mIsVideo == isVideo);
1585    }
1586
1587    private boolean queryThumbnail(SQLiteQueryBuilder qb, Uri uri, String table,
1588            String column, boolean hasThumbnailId) {
1589        qb.setTables(table);
1590        if (hasThumbnailId) {
1591            // For uri dispatched to this method, the 4th path segment is always
1592            // the thumbnail id.
1593            qb.appendWhere("_id = " + uri.getPathSegments().get(3));
1594            // client already knows which thumbnail it wants, bypass it.
1595            return true;
1596        }
1597        String origId = uri.getQueryParameter("orig_id");
1598        // We can't query ready_flag unless we know original id
1599        if (origId == null) {
1600            // this could be thumbnail query for other purpose, bypass it.
1601            return true;
1602        }
1603
1604        boolean needBlocking = "1".equals(uri.getQueryParameter("blocking"));
1605        boolean cancelRequest = "1".equals(uri.getQueryParameter("cancel"));
1606        Uri origUri = uri.buildUpon().encodedPath(
1607                uri.getPath().replaceFirst("thumbnails", "media"))
1608                .appendPath(origId).build();
1609
1610        if (needBlocking && !waitForThumbnailReady(origUri)) {
1611            Log.w(TAG, "original media doesn't exist or it's canceled.");
1612            return false;
1613        } else if (cancelRequest) {
1614            String groupId = uri.getQueryParameter("group_id");
1615            boolean isVideo = "video".equals(uri.getPathSegments().get(1));
1616            int pid = Binder.getCallingPid();
1617            long id = -1;
1618            long gid = -1;
1619
1620            try {
1621                id = Long.parseLong(origId);
1622                gid = Long.parseLong(groupId);
1623            } catch (NumberFormatException ex) {
1624                // invalid cancel request
1625                return false;
1626            }
1627
1628            synchronized (mMediaThumbQueue) {
1629                if (mCurrentThumbRequest != null &&
1630                        matchThumbRequest(mCurrentThumbRequest, pid, id, gid, isVideo)) {
1631                    synchronized (mCurrentThumbRequest) {
1632                        mCurrentThumbRequest.mState = MediaThumbRequest.State.CANCEL;
1633                        mCurrentThumbRequest.notifyAll();
1634                    }
1635                }
1636                for (MediaThumbRequest mtq : mMediaThumbQueue) {
1637                    if (matchThumbRequest(mtq, pid, id, gid, isVideo)) {
1638                        synchronized (mtq) {
1639                            mtq.mState = MediaThumbRequest.State.CANCEL;
1640                            mtq.notifyAll();
1641                        }
1642
1643                        mMediaThumbQueue.remove(mtq);
1644                    }
1645                }
1646            }
1647        }
1648
1649        if (origId != null) {
1650            qb.appendWhere(column + " = " + origId);
1651        }
1652        return true;
1653    }
1654    @SuppressWarnings("fallthrough")
1655    @Override
1656    public Cursor query(Uri uri, String[] projectionIn, String selection,
1657            String[] selectionArgs, String sort) {
1658        int table = URI_MATCHER.match(uri);
1659
1660        // Log.v(TAG, "query: uri="+uri+", selection="+selection);
1661        // handle MEDIA_SCANNER before calling getDatabaseForUri()
1662        if (table == MEDIA_SCANNER) {
1663            if (mMediaScannerVolume == null) {
1664                return null;
1665            } else {
1666                // create a cursor to return volume currently being scanned by the media scanner
1667                MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME});
1668                c.addRow(new String[] {mMediaScannerVolume});
1669                return c;
1670            }
1671        }
1672
1673        // Used temporarily (until we have unique media IDs) to get an identifier
1674        // for the current sd card, so that the music app doesn't have to use the
1675        // non-public getFatVolumeId method
1676        if (table == FS_ID) {
1677            MatrixCursor c = new MatrixCursor(new String[] {"fsid"});
1678            c.addRow(new Integer[] {mVolumeId});
1679            return c;
1680        }
1681
1682        String groupBy = null;
1683        DatabaseHelper database = getDatabaseForUri(uri);
1684        if (database == null) {
1685            return null;
1686        }
1687        SQLiteDatabase db = database.getReadableDatabase();
1688        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1689        String limit = uri.getQueryParameter("limit");
1690        String filter = uri.getQueryParameter("filter");
1691        String [] keywords = null;
1692        if (filter != null) {
1693            filter = Uri.decode(filter).trim();
1694            if (!TextUtils.isEmpty(filter)) {
1695                String [] searchWords = filter.split(" ");
1696                keywords = new String[searchWords.length];
1697                Collator col = Collator.getInstance();
1698                col.setStrength(Collator.PRIMARY);
1699                for (int i = 0; i < searchWords.length; i++) {
1700                    String key = MediaStore.Audio.keyFor(searchWords[i]);
1701                    key = key.replace("\\", "\\\\");
1702                    key = key.replace("%", "\\%");
1703                    key = key.replace("_", "\\_");
1704                    keywords[i] = key;
1705                }
1706            }
1707        }
1708
1709        boolean hasThumbnailId = false;
1710
1711        switch (table) {
1712            case IMAGES_MEDIA:
1713                qb.setTables("images");
1714                if (uri.getQueryParameter("distinct") != null)
1715                    qb.setDistinct(true);
1716
1717                // set the project map so that data dir is prepended to _data.
1718                //qb.setProjectionMap(mImagesProjectionMap, true);
1719                break;
1720
1721            case IMAGES_MEDIA_ID:
1722                qb.setTables("images");
1723                if (uri.getQueryParameter("distinct") != null)
1724                    qb.setDistinct(true);
1725
1726                // set the project map so that data dir is prepended to _data.
1727                //qb.setProjectionMap(mImagesProjectionMap, true);
1728                qb.appendWhere("_id = " + uri.getPathSegments().get(3));
1729                break;
1730
1731            case IMAGES_THUMBNAILS_ID:
1732                hasThumbnailId = true;
1733            case IMAGES_THUMBNAILS:
1734                if (!queryThumbnail(qb, uri, "thumbnails", "image_id", hasThumbnailId)) {
1735                    return null;
1736                }
1737                break;
1738
1739            case AUDIO_MEDIA:
1740                if (projectionIn != null && projectionIn.length == 1 &&  selectionArgs == null
1741                        && (selection == null || selection.equalsIgnoreCase("is_music=1")
1742                          || selection.equalsIgnoreCase("is_podcast=1") )
1743                        && projectionIn[0].equalsIgnoreCase("count(*)")
1744                        && keywords != null) {
1745                    //Log.i("@@@@", "taking fast path for counting songs");
1746                    qb.setTables("audio_meta");
1747                } else {
1748                    qb.setTables("audio");
1749                    for (int i = 0; keywords != null && i < keywords.length; i++) {
1750                        if (i > 0) {
1751                            qb.appendWhere(" AND ");
1752                        }
1753                        qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY +
1754                                "||" + MediaStore.Audio.Media.ALBUM_KEY +
1755                                "||" + MediaStore.Audio.Media.TITLE_KEY + " LIKE '%" +
1756                                keywords[i] + "%' ESCAPE '\\'");
1757                    }
1758                }
1759                break;
1760
1761            case AUDIO_MEDIA_ID:
1762                qb.setTables("audio");
1763                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
1764                break;
1765
1766            case AUDIO_MEDIA_ID_GENRES:
1767                qb.setTables("audio_genres");
1768                qb.appendWhere("_id IN (SELECT genre_id FROM " +
1769                        "audio_genres_map WHERE audio_id = " +
1770                        uri.getPathSegments().get(3) + ")");
1771                break;
1772
1773            case AUDIO_MEDIA_ID_GENRES_ID:
1774                qb.setTables("audio_genres");
1775                qb.appendWhere("_id=" + uri.getPathSegments().get(5));
1776                break;
1777
1778            case AUDIO_MEDIA_ID_PLAYLISTS:
1779                qb.setTables("audio_playlists");
1780                qb.appendWhere("_id IN (SELECT playlist_id FROM " +
1781                        "audio_playlists_map WHERE audio_id = " +
1782                        uri.getPathSegments().get(3) + ")");
1783                break;
1784
1785            case AUDIO_MEDIA_ID_PLAYLISTS_ID:
1786                qb.setTables("audio_playlists");
1787                qb.appendWhere("_id=" + uri.getPathSegments().get(5));
1788                break;
1789
1790            case AUDIO_GENRES:
1791                qb.setTables("audio_genres");
1792                break;
1793
1794            case AUDIO_GENRES_ID:
1795                qb.setTables("audio_genres");
1796                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
1797                break;
1798
1799            case AUDIO_GENRES_ID_MEMBERS:
1800                qb.setTables("audio");
1801                qb.appendWhere("_id IN (SELECT audio_id FROM " +
1802                        "audio_genres_map WHERE genre_id = " +
1803                        uri.getPathSegments().get(3) + ")");
1804                break;
1805
1806            case AUDIO_GENRES_ID_MEMBERS_ID:
1807                qb.setTables("audio");
1808                qb.appendWhere("_id=" + uri.getPathSegments().get(5));
1809                break;
1810
1811            case AUDIO_PLAYLISTS:
1812                qb.setTables("audio_playlists");
1813                break;
1814
1815            case AUDIO_PLAYLISTS_ID:
1816                qb.setTables("audio_playlists");
1817                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
1818                break;
1819
1820            case AUDIO_PLAYLISTS_ID_MEMBERS:
1821                if (projectionIn != null) {
1822                    for (int i = 0; i < projectionIn.length; i++) {
1823                        if (projectionIn[i].equals("_id")) {
1824                            projectionIn[i] = "audio_playlists_map._id AS _id";
1825                        }
1826                    }
1827                }
1828                qb.setTables("audio_playlists_map, audio");
1829                qb.appendWhere("audio._id = audio_id AND playlist_id = "
1830                        + uri.getPathSegments().get(3));
1831                for (int i = 0; keywords != null && i < keywords.length; i++) {
1832                    qb.appendWhere(" AND ");
1833                    qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY +
1834                            "||" + MediaStore.Audio.Media.ALBUM_KEY +
1835                            "||" + MediaStore.Audio.Media.TITLE_KEY +
1836                            " LIKE '%" + keywords[i] + "%' ESCAPE '\\'");
1837                }
1838                break;
1839
1840            case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
1841                qb.setTables("audio");
1842                qb.appendWhere("_id=" + uri.getPathSegments().get(5));
1843                break;
1844
1845            case VIDEO_MEDIA:
1846                qb.setTables("video");
1847                if (uri.getQueryParameter("distinct") != null) {
1848                    qb.setDistinct(true);
1849                }
1850                break;
1851            case VIDEO_MEDIA_ID:
1852                qb.setTables("video");
1853                if (uri.getQueryParameter("distinct") != null) {
1854                    qb.setDistinct(true);
1855                }
1856                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
1857                break;
1858
1859            case VIDEO_THUMBNAILS_ID:
1860                hasThumbnailId = true;
1861            case VIDEO_THUMBNAILS:
1862                if (!queryThumbnail(qb, uri, "videothumbnails", "video_id", hasThumbnailId)) {
1863                    return null;
1864                }
1865                break;
1866
1867            case AUDIO_ARTISTS:
1868                if (projectionIn != null && projectionIn.length == 1 &&  selectionArgs == null
1869                        && (selection == null || selection.length() == 0)
1870                        && projectionIn[0].equalsIgnoreCase("count(*)")
1871                        && keywords != null) {
1872                    //Log.i("@@@@", "taking fast path for counting artists");
1873                    qb.setTables("audio_meta");
1874                    projectionIn[0] = "count(distinct artist_id)";
1875                    qb.appendWhere("is_music=1");
1876                } else {
1877                    qb.setTables("artist_info");
1878                    for (int i = 0; keywords != null && i < keywords.length; i++) {
1879                        if (i > 0) {
1880                            qb.appendWhere(" AND ");
1881                        }
1882                        qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY +
1883                                " LIKE '%" + keywords[i] + "%' ESCAPE '\\'");
1884                    }
1885                }
1886                break;
1887
1888            case AUDIO_ARTISTS_ID:
1889                qb.setTables("artist_info");
1890                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
1891                break;
1892
1893            case AUDIO_ARTISTS_ID_ALBUMS:
1894                String aid = uri.getPathSegments().get(3);
1895                qb.setTables("audio LEFT OUTER JOIN album_art ON" +
1896                        " audio.album_id=album_art.album_id");
1897                qb.appendWhere("is_music=1 AND audio.album_id IN (SELECT album_id FROM " +
1898                        "artists_albums_map WHERE artist_id = " +
1899                         aid + ")");
1900                for (int i = 0; keywords != null && i < keywords.length; i++) {
1901                    qb.appendWhere(" AND ");
1902                    qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY +
1903                            "||" + MediaStore.Audio.Media.ALBUM_KEY +
1904                            " LIKE '%" + keywords[i] + "%' ESCAPE '\\'");
1905                }
1906                groupBy = "audio.album_id";
1907                sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST,
1908                        "count(CASE WHEN artist_id==" + aid + " THEN 'foo' ELSE NULL END) AS " +
1909                        MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST);
1910                qb.setProjectionMap(sArtistAlbumsMap);
1911                break;
1912
1913            case AUDIO_ALBUMS:
1914                if (projectionIn != null && projectionIn.length == 1 &&  selectionArgs == null
1915                        && (selection == null || selection.length() == 0)
1916                        && projectionIn[0].equalsIgnoreCase("count(*)")
1917                        && keywords != null) {
1918                    //Log.i("@@@@", "taking fast path for counting albums");
1919                    qb.setTables("audio_meta");
1920                    projectionIn[0] = "count(distinct album_id)";
1921                    qb.appendWhere("is_music=1");
1922                } else {
1923                    qb.setTables("album_info");
1924                    for (int i = 0; keywords != null && i < keywords.length; i++) {
1925                        if (i > 0) {
1926                            qb.appendWhere(" AND ");
1927                        }
1928                        qb.appendWhere(MediaStore.Audio.Media.ARTIST_KEY +
1929                                "||" + MediaStore.Audio.Media.ALBUM_KEY +
1930                                " LIKE '%" + keywords[i] + "%' ESCAPE '\\'");
1931                    }
1932                }
1933                break;
1934
1935            case AUDIO_ALBUMS_ID:
1936                qb.setTables("album_info");
1937                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
1938                break;
1939
1940            case AUDIO_ALBUMART_ID:
1941                qb.setTables("album_art");
1942                qb.appendWhere("album_id=" + uri.getPathSegments().get(3));
1943                break;
1944
1945            case AUDIO_SEARCH_LEGACY:
1946                Log.w(TAG, "Legacy media search Uri used. Please update your code.");
1947                // fall through
1948            case AUDIO_SEARCH_FANCY:
1949            case AUDIO_SEARCH_BASIC:
1950                return doAudioSearch(db, qb, uri, projectionIn, selection, selectionArgs, sort,
1951                        table, limit);
1952
1953            case FILES_ID:
1954            case MTP_OBJECTS_ID:
1955                qb.appendWhere("_id=" + uri.getPathSegments().get(2));
1956                // fall through
1957            case FILES:
1958            case MTP_OBJECTS:
1959                qb.setTables("files");
1960                break;
1961
1962            case MTP_OBJECT_REFERENCES:
1963                int handle = Integer.parseInt(uri.getPathSegments().get(2));
1964                return getObjectReferences(db, handle);
1965
1966            default:
1967                throw new IllegalStateException("Unknown URL: " + uri.toString());
1968        }
1969
1970        // Log.v(TAG, "query = "+ qb.buildQuery(projectionIn, selection, selectionArgs, groupBy, null, sort, limit));
1971        Cursor c = qb.query(db, projectionIn, selection,
1972                selectionArgs, groupBy, null, sort, limit);
1973
1974        if (c != null) {
1975            c.setNotificationUri(getContext().getContentResolver(), uri);
1976        }
1977
1978        return c;
1979    }
1980
1981    private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb,
1982            Uri uri, String[] projectionIn, String selection,
1983            String[] selectionArgs, String sort, int mode,
1984            String limit) {
1985
1986        String mSearchString = uri.getPath().endsWith("/") ? "" : uri.getLastPathSegment();
1987        mSearchString = mSearchString.replaceAll("  ", " ").trim().toLowerCase();
1988
1989        String [] searchWords = mSearchString.length() > 0 ?
1990                mSearchString.split(" ") : new String[0];
1991        String [] wildcardWords = new String[searchWords.length];
1992        Collator col = Collator.getInstance();
1993        col.setStrength(Collator.PRIMARY);
1994        int len = searchWords.length;
1995        for (int i = 0; i < len; i++) {
1996            // Because we match on individual words here, we need to remove words
1997            // like 'a' and 'the' that aren't part of the keys.
1998            String key = MediaStore.Audio.keyFor(searchWords[i]);
1999            key = key.replace("\\", "\\\\");
2000            key = key.replace("%", "\\%");
2001            key = key.replace("_", "\\_");
2002            wildcardWords[i] =
2003                (searchWords[i].equals("a") || searchWords[i].equals("an") ||
2004                        searchWords[i].equals("the")) ? "%" : "%" + key + "%";
2005        }
2006
2007        String where = "";
2008        for (int i = 0; i < searchWords.length; i++) {
2009            if (i == 0) {
2010                where = "match LIKE ? ESCAPE '\\'";
2011            } else {
2012                where += " AND match LIKE ? ESCAPE '\\'";
2013            }
2014        }
2015
2016        qb.setTables("search");
2017        String [] cols;
2018        if (mode == AUDIO_SEARCH_FANCY) {
2019            cols = mSearchColsFancy;
2020        } else if (mode == AUDIO_SEARCH_BASIC) {
2021            cols = mSearchColsBasic;
2022        } else {
2023            cols = mSearchColsLegacy;
2024        }
2025        return qb.query(db, cols, where, wildcardWords, null, null, null, limit);
2026    }
2027
2028    @Override
2029    public String getType(Uri url)
2030    {
2031        switch (URI_MATCHER.match(url)) {
2032            case IMAGES_MEDIA_ID:
2033            case AUDIO_MEDIA_ID:
2034            case AUDIO_GENRES_ID_MEMBERS_ID:
2035            case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
2036            case VIDEO_MEDIA_ID:
2037            case FILES_ID:
2038                Cursor c = null;
2039                try {
2040                    c = query(url, MIME_TYPE_PROJECTION, null, null, null);
2041                    if (c != null && c.getCount() == 1) {
2042                        c.moveToFirst();
2043                        String mimeType = c.getString(1);
2044                        c.deactivate();
2045                        return mimeType;
2046                    }
2047                } finally {
2048                    if (c != null) {
2049                        c.close();
2050                    }
2051                }
2052                break;
2053
2054            case IMAGES_MEDIA:
2055            case IMAGES_THUMBNAILS:
2056                return Images.Media.CONTENT_TYPE;
2057            case AUDIO_ALBUMART_ID:
2058            case IMAGES_THUMBNAILS_ID:
2059                return "image/jpeg";
2060
2061            case AUDIO_MEDIA:
2062            case AUDIO_GENRES_ID_MEMBERS:
2063            case AUDIO_PLAYLISTS_ID_MEMBERS:
2064                return Audio.Media.CONTENT_TYPE;
2065
2066            case AUDIO_GENRES:
2067            case AUDIO_MEDIA_ID_GENRES:
2068                return Audio.Genres.CONTENT_TYPE;
2069            case AUDIO_GENRES_ID:
2070            case AUDIO_MEDIA_ID_GENRES_ID:
2071                return Audio.Genres.ENTRY_CONTENT_TYPE;
2072            case AUDIO_PLAYLISTS:
2073            case AUDIO_MEDIA_ID_PLAYLISTS:
2074                return Audio.Playlists.CONTENT_TYPE;
2075            case AUDIO_PLAYLISTS_ID:
2076            case AUDIO_MEDIA_ID_PLAYLISTS_ID:
2077                return Audio.Playlists.ENTRY_CONTENT_TYPE;
2078
2079            case VIDEO_MEDIA:
2080                return Video.Media.CONTENT_TYPE;
2081        }
2082        throw new IllegalStateException("Unknown URL : " + url);
2083    }
2084
2085    /**
2086     * Ensures there is a file in the _data column of values, if one isn't
2087     * present a new file is created.
2088     *
2089     * @param initialValues the values passed to insert by the caller
2090     * @return the new values
2091     */
2092    private ContentValues ensureFile(boolean internal, ContentValues initialValues,
2093            String preferredExtension, String directoryName) {
2094        ContentValues values;
2095        String file = initialValues.getAsString("_data");
2096        file = mediaToExternalPath(file);
2097        if (TextUtils.isEmpty(file)) {
2098            file = generateFileName(internal, preferredExtension, directoryName);
2099            values = new ContentValues(initialValues);
2100            values.put("_data", file);
2101        } else {
2102            values = initialValues;
2103        }
2104
2105        if (!ensureFileExists(file)) {
2106            throw new IllegalStateException("Unable to create new file: " + file);
2107        }
2108        return values;
2109    }
2110
2111    private void sendObjectAdded(long objectHandle) {
2112        IMtpService mtpService = mMtpService;
2113        if (mtpService != null) {
2114            try {
2115                mtpService.sendObjectAdded((int)objectHandle);
2116            } catch (RemoteException e) {
2117                Log.e(TAG, "RemoteException in sendObjectAdded", e);
2118            }
2119        }
2120    }
2121
2122    private void sendObjectRemoved(long objectHandle) {
2123        IMtpService mtpService = mMtpService;
2124        if (mtpService != null) {
2125            try {
2126                mtpService.sendObjectRemoved((int)objectHandle);
2127            } catch (RemoteException e) {
2128                Log.e(TAG, "RemoteException in sendObjectRemoved", e);
2129            }
2130        }
2131    }
2132
2133    @Override
2134    public int bulkInsert(Uri uri, ContentValues values[]) {
2135        for (int i = 0; i < values.length; i++) {
2136            values[i] = mediaToExternalPath(values[i]);
2137        }
2138        int match = URI_MATCHER.match(uri);
2139        if (match == VOLUMES) {
2140            return super.bulkInsert(uri, values);
2141        }
2142        DatabaseHelper database = getDatabaseForUri(uri);
2143        if (database == null) {
2144            throw new UnsupportedOperationException(
2145                    "Unknown URI: " + uri);
2146        }
2147        SQLiteDatabase db = database.getWritableDatabase();
2148
2149        if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) {
2150            return playlistBulkInsert(db, uri, values);
2151        } else if (match == MTP_OBJECT_REFERENCES) {
2152            int handle = Integer.parseInt(uri.getPathSegments().get(2));
2153            return setObjectReferences(db, handle, values);
2154        }
2155
2156        db.beginTransaction();
2157        int numInserted = 0;
2158        try {
2159            int len = values.length;
2160            for (int i = 0; i < len; i++) {
2161                insertInternal(uri, match, values[i]);
2162            }
2163            numInserted = len;
2164            db.setTransactionSuccessful();
2165        } finally {
2166            db.endTransaction();
2167        }
2168        getContext().getContentResolver().notifyChange(uri, null);
2169        return numInserted;
2170    }
2171
2172    @Override
2173    public Uri insert(Uri uri, ContentValues initialValues)
2174    {
2175        initialValues = mediaToExternalPath(initialValues);
2176        int match = URI_MATCHER.match(uri);
2177        Uri newUri = insertInternal(uri, match, initialValues);
2178        // do not signal notification for MTP objects.
2179        // we will signal instead after file transfer is successful.
2180        if (newUri != null && match != MTP_OBJECTS) {
2181            getContext().getContentResolver().notifyChange(uri, null);
2182        }
2183        return newUri;
2184    }
2185
2186    private int playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[]) {
2187        DatabaseUtils.InsertHelper helper =
2188            new DatabaseUtils.InsertHelper(db, "audio_playlists_map");
2189        int audioidcolidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
2190        int playlistididx = helper.getColumnIndex(Audio.Playlists.Members.PLAYLIST_ID);
2191        int playorderidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
2192        long playlistId = Long.parseLong(uri.getPathSegments().get(3));
2193
2194        db.beginTransaction();
2195        int numInserted = 0;
2196        try {
2197            int len = values.length;
2198            for (int i = 0; i < len; i++) {
2199                helper.prepareForInsert();
2200                // getting the raw Object and converting it long ourselves saves
2201                // an allocation (the alternative is ContentValues.getAsLong, which
2202                // returns a Long object)
2203                long audioid = ((Number) values[i].get(
2204                        MediaStore.Audio.Playlists.Members.AUDIO_ID)).longValue();
2205                helper.bind(audioidcolidx, audioid);
2206                helper.bind(playlistididx, playlistId);
2207                // convert to int ourselves to save an allocation.
2208                int playorder = ((Number) values[i].get(
2209                        MediaStore.Audio.Playlists.Members.PLAY_ORDER)).intValue();
2210                helper.bind(playorderidx, playorder);
2211                helper.execute();
2212            }
2213            numInserted = len;
2214            db.setTransactionSuccessful();
2215        } finally {
2216            db.endTransaction();
2217            helper.close();
2218        }
2219        getContext().getContentResolver().notifyChange(uri, null);
2220        return numInserted;
2221    }
2222
2223    private long getParent(SQLiteDatabase db, String path) {
2224        int lastSlash = path.lastIndexOf('/');
2225        if (lastSlash > 0) {
2226            String parentPath = path.substring(0, lastSlash);
2227            if (parentPath.equals(mExternalStoragePath)) {
2228                return 0;
2229            }
2230            String [] selargs = { parentPath };
2231            Cursor c = db.query("files", null, MediaStore.MediaColumns.DATA + "=?",
2232                            selargs, null, null, null);
2233            try {
2234                if (c == null || c.getCount() == 0) {
2235                    // parent isn't in the database - so add it
2236                    ContentValues values = new ContentValues();
2237                    values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
2238                    values.put(FileColumns.DATA, parentPath);
2239                    values.put(FileColumns.PARENT, getParent(db, parentPath));
2240                    long parent = db.insert("files", FileColumns.DATE_MODIFIED, values);
2241                    sendObjectAdded(parent);
2242                    return parent;
2243                } else {
2244                    c.moveToFirst();
2245                    return c.getLong(0);
2246                }
2247            } finally {
2248                if (c != null) c.close();
2249            }
2250        } else {
2251            return 0;
2252        }
2253    }
2254
2255    private long insertFile(DatabaseHelper database, Uri uri, ContentValues initialValues, int mediaType,
2256            boolean notify) {
2257        SQLiteDatabase db = database.getWritableDatabase();
2258        ContentValues values = null;
2259
2260        switch (mediaType) {
2261            case FileColumns.MEDIA_TYPE_IMAGE: {
2262                values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/Camera");
2263
2264                values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
2265                String data = values.getAsString(MediaColumns.DATA);
2266                if (! values.containsKey(MediaColumns.DISPLAY_NAME)) {
2267                    computeDisplayName(data, values);
2268                }
2269                computeTakenTime(values);
2270                break;
2271            }
2272
2273            case FileColumns.MEDIA_TYPE_AUDIO: {
2274                // SQLite Views are read-only, so we need to deconstruct this
2275                // insert and do inserts into the underlying tables.
2276                // If doing this here turns out to be a performance bottleneck,
2277                // consider moving this to native code and using triggers on
2278                // the view.
2279                values = new ContentValues(initialValues);
2280
2281                // Insert the artist into the artist table and remove it from
2282                // the input values
2283                Object so = values.get("artist");
2284                String s = (so == null ? "" : so.toString());
2285                values.remove("artist");
2286                long artistRowId;
2287                HashMap<String, Long> artistCache = database.mArtistCache;
2288                String path = values.getAsString("_data");
2289                path = mediaToExternalPath(path);
2290                synchronized(artistCache) {
2291                    Long temp = artistCache.get(s);
2292                    if (temp == null) {
2293                        artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist",
2294                                s, s, path, 0, null, artistCache, uri);
2295                    } else {
2296                        artistRowId = temp.longValue();
2297                    }
2298                }
2299                String artist = s;
2300
2301                // Do the same for the album field
2302                so = values.get("album");
2303                s = (so == null ? "" : so.toString());
2304                values.remove("album");
2305                long albumRowId;
2306                HashMap<String, Long> albumCache = database.mAlbumCache;
2307                synchronized(albumCache) {
2308                    int albumhash = path.substring(0, path.lastIndexOf('/')).hashCode();
2309                    String cacheName = s + albumhash;
2310                    Long temp = albumCache.get(cacheName);
2311                    if (temp == null) {
2312                        albumRowId = getKeyIdForName(db, "albums", "album_key", "album",
2313                                s, cacheName, path, albumhash, artist, albumCache, uri);
2314                    } else {
2315                        albumRowId = temp;
2316                    }
2317                }
2318
2319                values.put("artist_id", Integer.toString((int)artistRowId));
2320                values.put("album_id", Integer.toString((int)albumRowId));
2321                so = values.getAsString("title");
2322                s = (so == null ? "" : so.toString());
2323                values.put("title_key", MediaStore.Audio.keyFor(s));
2324                // do a final trim of the title, in case it started with the special
2325                // "sort first" character (ascii \001)
2326                values.remove("title");
2327                values.put("title", s.trim());
2328
2329                computeDisplayName(values.getAsString("_data"), values);
2330                break;
2331            }
2332
2333            case FileColumns.MEDIA_TYPE_VIDEO: {
2334                values = ensureFile(database.mInternal, initialValues, ".3gp", "video");
2335                String data = values.getAsString("_data");
2336                computeDisplayName(data, values);
2337                computeTakenTime(values);
2338                break;
2339            }
2340        }
2341
2342        if (values == null) {
2343            values = new ContentValues(initialValues);
2344        }
2345        // compute bucket_id and bucket_display_name for all files
2346        String path = values.getAsString(MediaStore.MediaColumns.DATA);
2347        if (path != null) {
2348            computeBucketValues(path, values);
2349        }
2350        values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
2351
2352        long rowId = 0;
2353        Integer i = values.getAsInteger(
2354                MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID);
2355        if (i != null) {
2356            rowId = i.intValue();
2357            values = new ContentValues(values);
2358            values.remove(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID);
2359        }
2360
2361        String title = values.getAsString(MediaStore.MediaColumns.TITLE);
2362        if (title == null && path != null) {
2363            title = MediaFile.getFileTitle(path);
2364        }
2365        values.put(FileColumns.TITLE, title);
2366
2367        String mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE);
2368        Integer formatObject = values.getAsInteger(FileColumns.FORMAT);
2369        int format = (formatObject == null ? 0 : formatObject.intValue());
2370        if (format == 0) {
2371            if (path == null) {
2372                // special case device created playlists
2373                if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
2374                    values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST);
2375                    // create a file path for the benefit of MTP
2376                    path = mMediaStoragePath
2377                            + "/Playlists/" + values.getAsString(Audio.Playlists.NAME);
2378                    values.put(MediaStore.MediaColumns.DATA, path);
2379                    values.put(FileColumns.PARENT, getParent(db, path));
2380                } else {
2381                    Log.e(TAG, "path is null in insertObject()");
2382                }
2383            } else {
2384                format = MediaFile.getFormatCode(path, mimeType);
2385            }
2386        }
2387        if (format != 0) {
2388            values.put(FileColumns.FORMAT, format);
2389            if (mimeType == null) {
2390                mimeType = MediaFile.getMimeTypeForFormatCode(format);
2391            }
2392        }
2393
2394        if (mimeType == null) {
2395            mimeType = MediaFile.getMimeTypeForFile(path);
2396        }
2397        if (mimeType != null) {
2398            values.put(FileColumns.MIME_TYPE, mimeType);
2399
2400            if (mediaType == FileColumns.MEDIA_TYPE_NONE) {
2401                int fileType = MediaFile.getFileTypeForMimeType(mimeType);
2402                if (MediaFile.isAudioFileType(fileType)) {
2403                    mediaType = FileColumns.MEDIA_TYPE_AUDIO;
2404                } else if (MediaFile.isVideoFileType(fileType)) {
2405                    mediaType = FileColumns.MEDIA_TYPE_VIDEO;
2406                } else if (MediaFile.isImageFileType(fileType)) {
2407                    mediaType = FileColumns.MEDIA_TYPE_IMAGE;
2408                } else if (MediaFile.isPlayListFileType(fileType)) {
2409                    mediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
2410                }
2411            }
2412        }
2413        values.put(FileColumns.MEDIA_TYPE, mediaType);
2414
2415        if (rowId == 0) {
2416            if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
2417                String name = values.getAsString(Audio.Playlists.NAME);
2418                if (name == null && path == null) {
2419                    // MediaScanner will compute the name from the path if we have one
2420                    throw new IllegalArgumentException(
2421                            "no name was provided when inserting abstract playlist");
2422                }
2423            } else {
2424                if (path == null) {
2425                    // path might be null for playlists created on the device
2426                    // or transfered via MTP
2427                    throw new IllegalArgumentException(
2428                            "no path was provided when inserting new file");
2429                }
2430            }
2431
2432            Long size = values.getAsLong(MediaStore.MediaColumns.SIZE);
2433            if (size == null) {
2434                if (path != null) {
2435                    File file = new File(path);
2436                    values.put(FileColumns.SIZE, file.length());
2437                }
2438            } else {
2439                values.put(FileColumns.SIZE, size);
2440            }
2441
2442            Long parent = values.getAsLong(FileColumns.PARENT);
2443            if (parent == null) {
2444                if (path != null) {
2445                    long parentId = getParent(db, path);
2446                    values.put(FileColumns.PARENT, parentId);
2447                }
2448            } else {
2449                values.put(FileColumns.PARENT, parent);
2450            }
2451
2452            Integer modified = values.getAsInteger(MediaStore.MediaColumns.DATE_MODIFIED);
2453            if (modified != null) {
2454                values.put(FileColumns.DATE_MODIFIED, modified);
2455            }
2456
2457            rowId = db.insert("files", FileColumns.DATE_MODIFIED, values);
2458            if (LOCAL_LOGV) Log.v(TAG, "insertFile: values=" + values + " returned: " + rowId);
2459
2460            if (rowId != 0 && notify) {
2461                sendObjectAdded(rowId);
2462            }
2463        } else {
2464            db.update("files", values, FileColumns._ID + "=?",
2465                    new String[] { Long.toString(rowId) });
2466        }
2467
2468        return rowId;
2469    }
2470
2471    private Cursor getObjectReferences(SQLiteDatabase db, int handle) {
2472       Cursor c = db.query("files", mMediaTableColumns, "_id=?",
2473                new String[] {  Integer.toString(handle) },
2474                null, null, null);
2475        try {
2476            if (c != null && c.moveToNext()) {
2477                long playlistId = c.getLong(0);
2478                int mediaType = c.getInt(1);
2479                if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) {
2480                    // we only support object references for playlist objects
2481                    return null;
2482                }
2483                return db.rawQuery(OBJECT_REFERENCES_QUERY,
2484                        new String[] { Long.toString(playlistId) } );
2485            }
2486        } finally {
2487            if (c != null) {
2488                c.close();
2489            }
2490        }
2491        return null;
2492    }
2493
2494    private int setObjectReferences(SQLiteDatabase db, int handle, ContentValues values[]) {
2495        // first look up the media table and media ID for the object
2496        long playlistId = 0;
2497        Cursor c = db.query("files", mMediaTableColumns, "_id=?",
2498                new String[] {  Integer.toString(handle) },
2499                null, null, null);
2500        try {
2501            if (c != null && c.moveToNext()) {
2502                int mediaType = c.getInt(1);
2503                if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) {
2504                    // we only support object references for playlist objects
2505                    return 0;
2506                }
2507                playlistId = c.getLong(0);
2508            }
2509        } finally {
2510            if (c != null) {
2511                c.close();
2512            }
2513        }
2514        if (playlistId == 0) {
2515            return 0;
2516        }
2517
2518        // next delete any existing entries
2519       db.delete("audio_playlists_map", "playlist_id=?",
2520                new String[] { Long.toString(playlistId) });
2521
2522        // finally add the new entries
2523        int count = values.length;
2524        int added = 0;
2525        ContentValues[] valuesList = new ContentValues[count];
2526        for (int i = 0; i < count; i++) {
2527            // convert object ID to audio ID
2528            long audioId = 0;
2529            long objectId = values[i].getAsLong(MediaStore.MediaColumns._ID);
2530            c = db.query("files", mMediaTableColumns, "_id=?",
2531                    new String[] {  Long.toString(objectId) },
2532                    null, null, null);
2533            try {
2534                if (c != null && c.moveToNext()) {
2535                    int mediaType = c.getInt(1);
2536                    if (mediaType != FileColumns.MEDIA_TYPE_AUDIO) {
2537                        // we only allow audio files in playlists, so skip
2538                        continue;
2539                    }
2540                    audioId = c.getLong(0);
2541                }
2542            } finally {
2543                if (c != null) {
2544                    c.close();
2545                }
2546            }
2547            if (audioId != 0) {
2548                ContentValues v = new ContentValues();
2549                v.put(MediaStore.Audio.Playlists.Members.PLAYLIST_ID, playlistId);
2550                v.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId);
2551                v.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, added++);
2552                valuesList[i] = v;
2553            }
2554        }
2555        if (added < count) {
2556            // we weren't able to find everything on the list, so lets resize the array
2557            // and pass what we have.
2558            ContentValues[] newValues = new ContentValues[added];
2559            System.arraycopy(valuesList, 0, newValues, 0, added);
2560            valuesList = newValues;
2561        }
2562        return playlistBulkInsert(db,
2563                Audio.Playlists.Members.getContentUri(EXTERNAL_VOLUME, playlistId),
2564                valuesList);
2565    }
2566
2567    private Uri insertInternal(Uri uri, int match, ContentValues initialValues) {
2568        long rowId;
2569
2570        if (LOCAL_LOGV) Log.v(TAG, "insertInternal: "+uri+", initValues="+initialValues);
2571        // handle MEDIA_SCANNER before calling getDatabaseForUri()
2572        if (match == MEDIA_SCANNER) {
2573            mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);
2574            return MediaStore.getMediaScannerUri();
2575        }
2576
2577        Uri newUri = null;
2578        DatabaseHelper database = getDatabaseForUri(uri);
2579        if (database == null && match != VOLUMES) {
2580            throw new UnsupportedOperationException(
2581                    "Unknown URI: " + uri);
2582        }
2583        SQLiteDatabase db = (match == VOLUMES ? null : database.getWritableDatabase());
2584
2585        switch (match) {
2586            case IMAGES_MEDIA: {
2587                rowId = insertFile(database, uri, initialValues, FileColumns.MEDIA_TYPE_IMAGE, true);
2588                if (rowId > 0) {
2589                    newUri = ContentUris.withAppendedId(
2590                            Images.Media.getContentUri(uri.getPathSegments().get(0)), rowId);
2591                }
2592                break;
2593            }
2594
2595            // This will be triggered by requestMediaThumbnail (see getThumbnailUri)
2596            case IMAGES_THUMBNAILS: {
2597                ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg",
2598                        "DCIM/.thumbnails");
2599                rowId = db.insert("thumbnails", "name", values);
2600                if (rowId > 0) {
2601                    newUri = ContentUris.withAppendedId(Images.Thumbnails.
2602                            getContentUri(uri.getPathSegments().get(0)), rowId);
2603                }
2604                break;
2605            }
2606
2607            // This is currently only used by MICRO_KIND video thumbnail (see getThumbnailUri)
2608            case VIDEO_THUMBNAILS: {
2609                ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg",
2610                        "DCIM/.thumbnails");
2611                rowId = db.insert("videothumbnails", "name", values);
2612                if (rowId > 0) {
2613                    newUri = ContentUris.withAppendedId(Video.Thumbnails.
2614                            getContentUri(uri.getPathSegments().get(0)), rowId);
2615                }
2616                break;
2617            }
2618
2619            case AUDIO_MEDIA: {
2620                rowId = insertFile(database, uri, initialValues, FileColumns.MEDIA_TYPE_AUDIO, true);
2621                if (rowId > 0) {
2622                    newUri = ContentUris.withAppendedId(Audio.Media.getContentUri(uri.getPathSegments().get(0)), rowId);
2623                }
2624                break;
2625            }
2626
2627            case AUDIO_MEDIA_ID_GENRES: {
2628                Long audioId = Long.parseLong(uri.getPathSegments().get(2));
2629                ContentValues values = new ContentValues(initialValues);
2630                values.put(Audio.Genres.Members.AUDIO_ID, audioId);
2631                rowId = db.insert("audio_genres_map", "genre_id", values);
2632                if (rowId > 0) {
2633                    newUri = ContentUris.withAppendedId(uri, rowId);
2634                }
2635                break;
2636            }
2637
2638            case AUDIO_MEDIA_ID_PLAYLISTS: {
2639                Long audioId = Long.parseLong(uri.getPathSegments().get(2));
2640                ContentValues values = new ContentValues(initialValues);
2641                values.put(Audio.Playlists.Members.AUDIO_ID, audioId);
2642                rowId = db.insert("audio_playlists_map", "playlist_id",
2643                        values);
2644                if (rowId > 0) {
2645                    newUri = ContentUris.withAppendedId(uri, rowId);
2646                }
2647                break;
2648            }
2649
2650            case AUDIO_GENRES: {
2651                rowId = db.insert("audio_genres", "audio_id", initialValues);
2652                if (rowId > 0) {
2653                    newUri = ContentUris.withAppendedId(Audio.Genres.getContentUri(uri.getPathSegments().get(0)), rowId);
2654                }
2655                break;
2656            }
2657
2658            case AUDIO_GENRES_ID_MEMBERS: {
2659                Long genreId = Long.parseLong(uri.getPathSegments().get(3));
2660                ContentValues values = new ContentValues(initialValues);
2661                values.put(Audio.Genres.Members.GENRE_ID, genreId);
2662                rowId = db.insert("audio_genres_map", "genre_id", values);
2663                if (rowId > 0) {
2664                    newUri = ContentUris.withAppendedId(uri, rowId);
2665                }
2666                break;
2667            }
2668
2669            case AUDIO_PLAYLISTS: {
2670                ContentValues values = new ContentValues(initialValues);
2671                values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
2672                rowId = insertFile(database, uri, values, FileColumns.MEDIA_TYPE_PLAYLIST, true);
2673                if (rowId > 0) {
2674                    newUri = ContentUris.withAppendedId(Audio.Playlists.getContentUri(uri.getPathSegments().get(0)), rowId);
2675                }
2676                break;
2677            }
2678
2679            case AUDIO_PLAYLISTS_ID:
2680            case AUDIO_PLAYLISTS_ID_MEMBERS: {
2681                Long playlistId = Long.parseLong(uri.getPathSegments().get(3));
2682                ContentValues values = new ContentValues(initialValues);
2683                values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId);
2684                rowId = db.insert("audio_playlists_map", "playlist_id", values);
2685                if (rowId > 0) {
2686                    newUri = ContentUris.withAppendedId(uri, rowId);
2687                }
2688                break;
2689            }
2690
2691            case VIDEO_MEDIA: {
2692                rowId = insertFile(database, uri, initialValues, FileColumns.MEDIA_TYPE_VIDEO, true);
2693                if (rowId > 0) {
2694                    newUri = ContentUris.withAppendedId(Video.Media.getContentUri(
2695                            uri.getPathSegments().get(0)), rowId);
2696                }
2697                break;
2698            }
2699
2700            case AUDIO_ALBUMART: {
2701                if (database.mInternal) {
2702                    throw new UnsupportedOperationException("no internal album art allowed");
2703                }
2704                ContentValues values = null;
2705                try {
2706                    values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER);
2707                } catch (IllegalStateException ex) {
2708                    // probably no more room to store albumthumbs
2709                    values = initialValues;
2710                }
2711                rowId = db.insert("album_art", "_data", values);
2712                if (rowId > 0) {
2713                    newUri = ContentUris.withAppendedId(uri, rowId);
2714                }
2715                break;
2716            }
2717
2718            case VOLUMES:
2719                return attachVolume(initialValues.getAsString("name"));
2720
2721            case FILES:
2722                rowId = insertFile(database, uri, initialValues,
2723                        FileColumns.MEDIA_TYPE_NONE, true);
2724                if (rowId > 0) {
2725                    newUri = Files.getContentUri(uri.getPathSegments().get(0), rowId);
2726                }
2727                break;
2728
2729            case MTP_OBJECTS:
2730                // don't send a notification if the insert originated from MTP
2731                rowId = insertFile(database, uri, initialValues,
2732                        FileColumns.MEDIA_TYPE_NONE, false);
2733                if (rowId > 0) {
2734                    newUri = Files.getMtpObjectsUri(uri.getPathSegments().get(0), rowId);
2735                }
2736                break;
2737
2738            default:
2739                throw new UnsupportedOperationException("Invalid URI " + uri);
2740        }
2741
2742        return newUri;
2743    }
2744
2745    @Override
2746    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
2747                throws OperationApplicationException {
2748
2749        // The operations array provides no overall information about the URI(s) being operated
2750        // on, so begin a transaction for ALL of the databases.
2751        DatabaseHelper ihelper = getDatabaseForUri(MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
2752        DatabaseHelper ehelper = getDatabaseForUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
2753        SQLiteDatabase idb = ihelper.getWritableDatabase();
2754        idb.beginTransaction();
2755        SQLiteDatabase edb = null;
2756        if (ehelper != null) {
2757            edb = ehelper.getWritableDatabase();
2758            edb.beginTransaction();
2759        }
2760        try {
2761            ContentProviderResult[] result = super.applyBatch(operations);
2762            idb.setTransactionSuccessful();
2763            if (edb != null) {
2764                edb.setTransactionSuccessful();
2765            }
2766            // Rather than sending targeted change notifications for every Uri
2767            // affected by the batch operation, just invalidate the entire internal
2768            // and external name space.
2769            ContentResolver res = getContext().getContentResolver();
2770            res.notifyChange(Uri.parse("content://media/"), null);
2771            return result;
2772        } finally {
2773            idb.endTransaction();
2774            if (edb != null) {
2775                edb.endTransaction();
2776            }
2777        }
2778    }
2779
2780
2781    private MediaThumbRequest requestMediaThumbnail(String path, Uri uri, int priority, long magic) {
2782        path = mediaToExternalPath(path);
2783        synchronized (mMediaThumbQueue) {
2784            MediaThumbRequest req = null;
2785            try {
2786                req = new MediaThumbRequest(
2787                        getContext().getContentResolver(), path, uri, priority, magic);
2788                mMediaThumbQueue.add(req);
2789                // Trigger the handler.
2790                Message msg = mThumbHandler.obtainMessage(IMAGE_THUMB);
2791                msg.sendToTarget();
2792            } catch (Throwable t) {
2793                Log.w(TAG, t);
2794            }
2795            return req;
2796        }
2797    }
2798
2799    private String generateFileName(boolean internal, String preferredExtension, String directoryName)
2800    {
2801        // create a random file
2802        String name = String.valueOf(System.currentTimeMillis());
2803
2804        if (internal) {
2805            throw new UnsupportedOperationException("Writing to internal storage is not supported.");
2806//            return Environment.getDataDirectory()
2807//                + "/" + directoryName + "/" + name + preferredExtension;
2808        } else {
2809            return mMediaStoragePath + "/" + directoryName + "/" + name + preferredExtension;
2810        }
2811    }
2812
2813    private boolean ensureFileExists(String path) {
2814        File file = new File(path);
2815        if (file.exists()) {
2816            return true;
2817        } else {
2818            // we will not attempt to create the first directory in the path
2819            // (for example, do not create /sdcard if the SD card is not mounted)
2820            int secondSlash = path.indexOf('/', 1);
2821            if (secondSlash < 1) return false;
2822            String directoryPath = path.substring(0, secondSlash);
2823            File directory = new File(directoryPath);
2824            if (!directory.exists())
2825                return false;
2826            File parent = file.getParentFile();
2827            // create parent directories if necessary, and ensure they have correct permissions
2828            if (!parent.exists()) {
2829                parent.mkdirs();
2830                String parentPath = parent.getPath();
2831                if (parentPath.startsWith(mMediaStoragePath)) {
2832                    while (parent != null && !mMediaStoragePath.equals(parentPath)) {
2833                        FileUtils.setPermissions(parentPath, 0775, Process.myUid(),
2834                                Process.SDCARD_RW_GID);
2835                        parent = parent.getParentFile();
2836                        parentPath = parent.getPath();
2837                    }
2838                }
2839
2840            }
2841            try {
2842                if (file.createNewFile()) {
2843                    // file should be writeable for SDCARD_RW group and world readable
2844                    FileUtils.setPermissions(file.getPath(), 0664, Process.myUid(),
2845                            Process.SDCARD_RW_GID);
2846                    return true;
2847                }
2848            } catch(IOException ioe) {
2849                Log.e(TAG, "File creation failed", ioe);
2850            }
2851            return false;
2852        }
2853    }
2854
2855    private static final class GetTableAndWhereOutParameter {
2856        public String table;
2857        public String where;
2858    }
2859
2860    static final GetTableAndWhereOutParameter sGetTableAndWhereParam =
2861            new GetTableAndWhereOutParameter();
2862
2863    private void getTableAndWhere(Uri uri, int match, String userWhere,
2864            GetTableAndWhereOutParameter out) {
2865        String where = null;
2866        switch (match) {
2867            case IMAGES_MEDIA:
2868                out.table = "files";
2869                where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_IMAGE;
2870                break;
2871
2872            case IMAGES_MEDIA_ID:
2873                out.table = "files";
2874                where = "_id = " + uri.getPathSegments().get(3);
2875                break;
2876
2877            case IMAGES_THUMBNAILS_ID:
2878                where = "_id=" + uri.getPathSegments().get(3);
2879            case IMAGES_THUMBNAILS:
2880                out.table = "thumbnails";
2881                break;
2882
2883            case AUDIO_MEDIA:
2884                out.table = "files";
2885                where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_AUDIO;
2886                break;
2887
2888            case AUDIO_MEDIA_ID:
2889                out.table = "files";
2890                where = "_id=" + uri.getPathSegments().get(3);
2891                break;
2892
2893            case AUDIO_MEDIA_ID_GENRES:
2894                out.table = "audio_genres";
2895                where = "audio_id=" + uri.getPathSegments().get(3);
2896                break;
2897
2898            case AUDIO_MEDIA_ID_GENRES_ID:
2899                out.table = "audio_genres";
2900                where = "audio_id=" + uri.getPathSegments().get(3) +
2901                        " AND genre_id=" + uri.getPathSegments().get(5);
2902               break;
2903
2904            case AUDIO_MEDIA_ID_PLAYLISTS:
2905                out.table = "audio_playlists";
2906                where = "audio_id=" + uri.getPathSegments().get(3);
2907                break;
2908
2909            case AUDIO_MEDIA_ID_PLAYLISTS_ID:
2910                out.table = "audio_playlists";
2911                where = "audio_id=" + uri.getPathSegments().get(3) +
2912                        " AND playlists_id=" + uri.getPathSegments().get(5);
2913                break;
2914
2915            case AUDIO_GENRES:
2916                out.table = "audio_genres";
2917                break;
2918
2919            case AUDIO_GENRES_ID:
2920                out.table = "audio_genres";
2921                where = "_id=" + uri.getPathSegments().get(3);
2922                break;
2923
2924            case AUDIO_GENRES_ID_MEMBERS:
2925                out.table = "audio_genres";
2926                where = "genre_id=" + uri.getPathSegments().get(3);
2927                break;
2928
2929            case AUDIO_GENRES_ID_MEMBERS_ID:
2930                out.table = "audio_genres";
2931                where = "genre_id=" + uri.getPathSegments().get(3) +
2932                        " AND audio_id =" + uri.getPathSegments().get(5);
2933                break;
2934
2935            case AUDIO_PLAYLISTS:
2936                out.table = "files";
2937                where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_PLAYLIST;
2938                break;
2939
2940            case AUDIO_PLAYLISTS_ID:
2941                out.table = "files";
2942                where = "_id=" + uri.getPathSegments().get(3);
2943                break;
2944
2945            case AUDIO_PLAYLISTS_ID_MEMBERS:
2946                out.table = "audio_playlists_map";
2947                where = "playlist_id=" + uri.getPathSegments().get(3);
2948                break;
2949
2950            case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
2951                out.table = "audio_playlists_map";
2952                where = "playlist_id=" + uri.getPathSegments().get(3) +
2953                        " AND _id=" + uri.getPathSegments().get(5);
2954                break;
2955
2956            case AUDIO_ALBUMART_ID:
2957                out.table = "album_art";
2958                where = "album_id=" + uri.getPathSegments().get(3);
2959                break;
2960
2961            case VIDEO_MEDIA:
2962                out.table = "files";
2963                where = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_VIDEO;
2964                break;
2965
2966            case VIDEO_MEDIA_ID:
2967                out.table = "files";
2968                where = "_id=" + uri.getPathSegments().get(3);
2969                break;
2970
2971            case VIDEO_THUMBNAILS_ID:
2972                where = "_id=" + uri.getPathSegments().get(3);
2973            case VIDEO_THUMBNAILS:
2974                out.table = "videothumbnails";
2975                break;
2976
2977            case FILES_ID:
2978            case MTP_OBJECTS_ID:
2979                where = "_id=" + uri.getPathSegments().get(2);
2980            case FILES:
2981            case MTP_OBJECTS:
2982                out.table = "files";
2983                break;
2984
2985            default:
2986                throw new UnsupportedOperationException(
2987                        "Unknown or unsupported URL: " + uri.toString());
2988        }
2989
2990        // Add in the user requested WHERE clause, if needed
2991        if (!TextUtils.isEmpty(userWhere)) {
2992            if (!TextUtils.isEmpty(where)) {
2993                out.where = where + " AND (" + userWhere + ")";
2994            } else {
2995                out.where = userWhere;
2996            }
2997        } else {
2998            out.where = where;
2999        }
3000    }
3001
3002    @Override
3003    public int delete(Uri uri, String userWhere, String[] whereArgs) {
3004        int count;
3005        int match = URI_MATCHER.match(uri);
3006
3007        // handle MEDIA_SCANNER before calling getDatabaseForUri()
3008        if (match == MEDIA_SCANNER) {
3009            if (mMediaScannerVolume == null) {
3010                return 0;
3011            }
3012            mMediaScannerVolume = null;
3013            return 1;
3014        }
3015
3016        if (match != VOLUMES_ID) {
3017            DatabaseHelper database = getDatabaseForUri(uri);
3018            if (database == null) {
3019                throw new UnsupportedOperationException(
3020                        "Unknown URI: " + uri);
3021            }
3022            SQLiteDatabase db = database.getWritableDatabase();
3023
3024            synchronized (sGetTableAndWhereParam) {
3025                getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam);
3026                switch (match) {
3027                    case MTP_OBJECTS:
3028                    case MTP_OBJECTS_ID:
3029                        try {
3030                            // don't send objectRemoved event since this originated from MTP
3031                            mDisableMtpObjectCallbacks = true;
3032                             count = db.delete("files", sGetTableAndWhereParam.where, whereArgs);
3033                        } finally {
3034                            mDisableMtpObjectCallbacks = false;
3035                        }
3036                        break;
3037
3038                    default:
3039                        count = db.delete(sGetTableAndWhereParam.table,
3040                                sGetTableAndWhereParam.where, whereArgs);
3041                        break;
3042                }
3043                // Since there are multiple Uris that can refer to the same files
3044                // and deletes can affect other objects in storage (like subdirectories
3045                // or playlists) we will notify a change on the entire volume to make
3046                // sure no listeners miss the notification.
3047                String volume = uri.getPathSegments().get(0);
3048                Uri notifyUri = Uri.parse("content://" + MediaStore.AUTHORITY + "/" + volume);
3049                getContext().getContentResolver().notifyChange(notifyUri, null);
3050            }
3051        } else {
3052            detachVolume(uri);
3053            count = 1;
3054        }
3055
3056        return count;
3057    }
3058
3059    @Override
3060    public int update(Uri uri, ContentValues initialValues, String userWhere,
3061            String[] whereArgs) {
3062        initialValues = mediaToExternalPath(initialValues);
3063        int count;
3064        // Log.v(TAG, "update for uri="+uri+", initValues="+initialValues);
3065        int match = URI_MATCHER.match(uri);
3066        DatabaseHelper database = getDatabaseForUri(uri);
3067        if (database == null) {
3068            throw new UnsupportedOperationException(
3069                    "Unknown URI: " + uri);
3070        }
3071        SQLiteDatabase db = database.getWritableDatabase();
3072
3073        synchronized (sGetTableAndWhereParam) {
3074            getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam);
3075
3076            // special case renaming directories via MTP.
3077            // in this case we must update all paths in the database with
3078            // the directory name as a prefix
3079            if ((match == MTP_OBJECTS || match == MTP_OBJECTS_ID)
3080                    && initialValues != null && initialValues.size() == 1) {
3081                String oldPath = null;
3082                String newPath = initialValues.getAsString("_data");
3083                // MtpDatabase will rename the directory first, so we test the new file name
3084                if (newPath != null && (new File(newPath)).isDirectory()) {
3085                    Cursor cursor = db.query(sGetTableAndWhereParam.table, PATH_PROJECTION,
3086                        userWhere, whereArgs, null, null, null);
3087                    try {
3088                        if (cursor != null && cursor.moveToNext()) {
3089                            oldPath = cursor.getString(1);
3090                        }
3091                    } finally {
3092                        if (cursor != null) cursor.close();
3093                    }
3094                    if (oldPath != null) {
3095                        // first rename the row for the directory
3096                        count = db.update(sGetTableAndWhereParam.table, initialValues,
3097                                sGetTableAndWhereParam.where, whereArgs);
3098                        if (count > 0) {
3099                            // then update the paths of any files and folders contained in the directory.
3100                            db.execSQL("UPDATE files SET _data=REPLACE(_data, '"
3101                                    + oldPath + "/','" + newPath + "/');");
3102                        }
3103
3104                        if (count > 0 && !db.inTransaction()) {
3105                            getContext().getContentResolver().notifyChange(uri, null);
3106                        }
3107                        return count;
3108                    }
3109                }
3110            }
3111
3112            switch (match) {
3113                case AUDIO_MEDIA:
3114                case AUDIO_MEDIA_ID:
3115                    {
3116                        ContentValues values = new ContentValues(initialValues);
3117
3118                        // Insert the artist into the artist table and remove it from
3119                        // the input values
3120                        String artist = values.getAsString("artist");
3121                        values.remove("artist");
3122                        if (artist != null) {
3123                            long artistRowId;
3124                            HashMap<String, Long> artistCache = database.mArtistCache;
3125                            synchronized(artistCache) {
3126                                Long temp = artistCache.get(artist);
3127                                if (temp == null) {
3128                                    artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist",
3129                                            artist, artist, null, 0, null, artistCache, uri);
3130                                } else {
3131                                    artistRowId = temp.longValue();
3132                                }
3133                            }
3134                            values.put("artist_id", Integer.toString((int)artistRowId));
3135                        }
3136
3137                        // Do the same for the album field.
3138                        String so = values.getAsString("album");
3139                        values.remove("album");
3140                        if (so != null) {
3141                            String path = values.getAsString("_data");
3142                            int albumHash = 0;
3143                            if (path == null) {
3144                                // If the path is null, we don't have a hash for the file in question.
3145                                Log.w(TAG, "Update without specified path.");
3146                            } else {
3147                                path = mediaToExternalPath(path);
3148                                albumHash = path.substring(0, path.lastIndexOf('/')).hashCode();
3149                            }
3150                            String s = so.toString();
3151                            long albumRowId;
3152                            HashMap<String, Long> albumCache = database.mAlbumCache;
3153                            synchronized(albumCache) {
3154                                String cacheName = s + albumHash;
3155                                Long temp = albumCache.get(cacheName);
3156                                if (temp == null) {
3157                                    albumRowId = getKeyIdForName(db, "albums", "album_key", "album",
3158                                            s, cacheName, path, albumHash, artist, albumCache, uri);
3159                                } else {
3160                                    albumRowId = temp.longValue();
3161                                }
3162                            }
3163                            values.put("album_id", Integer.toString((int)albumRowId));
3164                        }
3165
3166                        // don't allow the title_key field to be updated directly
3167                        values.remove("title_key");
3168                        // If the title field is modified, update the title_key
3169                        so = values.getAsString("title");
3170                        if (so != null) {
3171                            String s = so.toString();
3172                            values.put("title_key", MediaStore.Audio.keyFor(s));
3173                            // do a final trim of the title, in case it started with the special
3174                            // "sort first" character (ascii \001)
3175                            values.remove("title");
3176                            values.put("title", s.trim());
3177                        }
3178
3179                        count = db.update(sGetTableAndWhereParam.table, values,
3180                                sGetTableAndWhereParam.where, whereArgs);
3181                    }
3182                    break;
3183                case IMAGES_MEDIA:
3184                case IMAGES_MEDIA_ID:
3185                case VIDEO_MEDIA:
3186                case VIDEO_MEDIA_ID:
3187                    {
3188                        ContentValues values = new ContentValues(initialValues);
3189                        // Don't allow bucket id or display name to be updated directly.
3190                        // The same names are used for both images and table columns, so
3191                        // we use the ImageColumns constants here.
3192                        values.remove(ImageColumns.BUCKET_ID);
3193                        values.remove(ImageColumns.BUCKET_DISPLAY_NAME);
3194                        // If the data is being modified update the bucket values
3195                        String data = values.getAsString(MediaColumns.DATA);
3196                        if (data != null) {
3197                            computeBucketValues(data, values);
3198                        }
3199                        computeTakenTime(values);
3200                        count = db.update(sGetTableAndWhereParam.table, values,
3201                                sGetTableAndWhereParam.where, whereArgs);
3202                        // if this is a request from MediaScanner, DATA should contains file path
3203                        // we only process update request from media scanner, otherwise the requests
3204                        // could be duplicate.
3205                        if (count > 0 && values.getAsString(MediaStore.MediaColumns.DATA) != null) {
3206                            Cursor c = db.query(sGetTableAndWhereParam.table,
3207                                    READY_FLAG_PROJECTION, sGetTableAndWhereParam.where,
3208                                    whereArgs, null, null, null);
3209                            if (c != null) {
3210                                try {
3211                                    while (c.moveToNext()) {
3212                                        long magic = c.getLong(2);
3213                                        if (magic == 0) {
3214                                            requestMediaThumbnail(c.getString(1), uri,
3215                                                    MediaThumbRequest.PRIORITY_NORMAL, 0);
3216                                        }
3217                                    }
3218                                } finally {
3219                                    c.close();
3220                                }
3221                            }
3222                        }
3223                    }
3224                    break;
3225
3226                case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
3227                    String moveit = uri.getQueryParameter("move");
3228                    if (moveit != null) {
3229                        String key = MediaStore.Audio.Playlists.Members.PLAY_ORDER;
3230                        if (initialValues.containsKey(key)) {
3231                            int newpos = initialValues.getAsInteger(key);
3232                            List <String> segments = uri.getPathSegments();
3233                            long playlist = Long.valueOf(segments.get(3));
3234                            int oldpos = Integer.valueOf(segments.get(5));
3235                            return movePlaylistEntry(db, playlist, oldpos, newpos);
3236                        }
3237                        throw new IllegalArgumentException("Need to specify " + key +
3238                                " when using 'move' parameter");
3239                    }
3240                    // fall through
3241                default:
3242                    count = db.update(sGetTableAndWhereParam.table, initialValues,
3243                        sGetTableAndWhereParam.where, whereArgs);
3244                    break;
3245            }
3246        }
3247        // in a transaction, the code that began the transaction should be taking
3248        // care of notifications once it ends the transaction successfully
3249        if (count > 0 && !db.inTransaction()) {
3250            getContext().getContentResolver().notifyChange(uri, null);
3251        }
3252        return count;
3253    }
3254
3255    private int movePlaylistEntry(SQLiteDatabase db, long playlist, int from, int to) {
3256        if (from == to) {
3257            return 0;
3258        }
3259        db.beginTransaction();
3260        try {
3261            int numlines = 0;
3262            db.execSQL("UPDATE audio_playlists_map SET play_order=-1" +
3263                    " WHERE play_order=" + from +
3264                    " AND playlist_id=" + playlist);
3265            // We could just run both of the next two statements, but only one of
3266            // of them will actually do anything, so might as well skip the compile
3267            // and execute steps.
3268            if (from  < to) {
3269                db.execSQL("UPDATE audio_playlists_map SET play_order=play_order-1" +
3270                        " WHERE play_order<=" + to + " AND play_order>" + from +
3271                        " AND playlist_id=" + playlist);
3272                numlines = to - from + 1;
3273            } else {
3274                db.execSQL("UPDATE audio_playlists_map SET play_order=play_order+1" +
3275                        " WHERE play_order>=" + to + " AND play_order<" + from +
3276                        " AND playlist_id=" + playlist);
3277                numlines = from - to + 1;
3278            }
3279            db.execSQL("UPDATE audio_playlists_map SET play_order=" + to +
3280                    " WHERE play_order=-1 AND playlist_id=" + playlist);
3281            db.setTransactionSuccessful();
3282            Uri uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI
3283                    .buildUpon().appendEncodedPath(String.valueOf(playlist)).build();
3284            getContext().getContentResolver().notifyChange(uri, null);
3285            return numlines;
3286        } finally {
3287            db.endTransaction();
3288        }
3289    }
3290
3291    private static final String[] openFileColumns = new String[] {
3292        MediaStore.MediaColumns.DATA,
3293    };
3294
3295    /* Same as ContentProvider.openFileHelper, except we will convert paths
3296       in external storage to internal media storage to avoid Fuse overhead.
3297     */
3298    private final ParcelFileDescriptor doOpenFile(Uri uri,
3299            String mode) throws FileNotFoundException {
3300        Cursor c = query(uri, new String[]{"_data"}, null, null, null);
3301        int count = (c != null) ? c.getCount() : 0;
3302        if (count != 1) {
3303            // If there is not exactly one result, throw an appropriate
3304            // exception.
3305            if (c != null) {
3306                c.close();
3307            }
3308            if (count == 0) {
3309                throw new FileNotFoundException("No entry for " + uri);
3310            }
3311            throw new FileNotFoundException("Multiple items at " + uri);
3312        }
3313
3314        c.moveToFirst();
3315        int i = c.getColumnIndex("_data");
3316        String path = (i >= 0 ? c.getString(i) : null);
3317        c.close();
3318        if (path == null) {
3319            throw new FileNotFoundException("Column _data not found.");
3320        }
3321
3322        path = externalToMediaPath(path);
3323        int modeBits = ContentResolver.modeToMode(uri, mode);
3324        return ParcelFileDescriptor.open(new File(path), modeBits);
3325    }
3326
3327    @Override
3328    public ParcelFileDescriptor openFile(Uri uri, String mode)
3329            throws FileNotFoundException {
3330
3331        ParcelFileDescriptor pfd = null;
3332
3333        if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_FILE_ID) {
3334            // get album art for the specified media file
3335            DatabaseHelper database = getDatabaseForUri(uri);
3336            if (database == null) {
3337                throw new IllegalStateException("Couldn't open database for " + uri);
3338            }
3339            SQLiteDatabase db = database.getReadableDatabase();
3340            SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
3341            int songid = Integer.parseInt(uri.getPathSegments().get(3));
3342            qb.setTables("audio_meta");
3343            qb.appendWhere("_id=" + songid);
3344            Cursor c = qb.query(db,
3345                    new String [] {
3346                        MediaStore.Audio.Media.DATA,
3347                        MediaStore.Audio.Media.ALBUM_ID },
3348                    null, null, null, null, null);
3349            if (c.moveToFirst()) {
3350                String audiopath = c.getString(0);
3351                int albumid = c.getInt(1);
3352                // Try to get existing album art for this album first, which
3353                // could possibly have been obtained from a different file.
3354                // If that fails, try to get it from this specific file.
3355                Uri newUri = ContentUris.withAppendedId(ALBUMART_URI, albumid);
3356                try {
3357                    pfd = openFile(newUri, mode);  // recursive call
3358                } catch (FileNotFoundException ex) {
3359                    // That didn't work, now try to get it from the specific file
3360                    pfd = getThumb(db, audiopath, albumid, null);
3361                }
3362            }
3363            c.close();
3364            return pfd;
3365        }
3366
3367        try {
3368            pfd = openFileHelper(uri, mode);
3369        } catch (FileNotFoundException ex) {
3370            if (mode.contains("w")) {
3371                // if the file couldn't be created, we shouldn't extract album art
3372                throw ex;
3373            }
3374
3375            if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_ID) {
3376                // Tried to open an album art file which does not exist. Regenerate.
3377                DatabaseHelper database = getDatabaseForUri(uri);
3378                if (database == null) {
3379                    throw ex;
3380                }
3381                SQLiteDatabase db = database.getReadableDatabase();
3382                SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
3383                int albumid = Integer.parseInt(uri.getPathSegments().get(3));
3384                qb.setTables("audio_meta");
3385                qb.appendWhere("album_id=" + albumid);
3386                Cursor c = qb.query(db,
3387                        new String [] {
3388                            MediaStore.Audio.Media.DATA },
3389                        null, null, null, null, MediaStore.Audio.Media.TRACK);
3390                if (c.moveToFirst()) {
3391                    String audiopath = c.getString(0);
3392                    pfd = getThumb(db, audiopath, albumid, uri);
3393                }
3394                c.close();
3395            }
3396            if (pfd == null) {
3397                throw ex;
3398            }
3399        }
3400        return pfd;
3401    }
3402
3403    private class ThumbData {
3404        SQLiteDatabase db;
3405        String path;
3406        long album_id;
3407        Uri albumart_uri;
3408    }
3409
3410    private void makeThumbAsync(SQLiteDatabase db, String path, long album_id) {
3411        synchronized (mPendingThumbs) {
3412            if (mPendingThumbs.contains(path)) {
3413                // There's already a request to make an album art thumbnail
3414                // for this audio file in the queue.
3415                return;
3416            }
3417
3418            mPendingThumbs.add(path);
3419        }
3420
3421        ThumbData d = new ThumbData();
3422        d.db = db;
3423        d.path = path;
3424        d.album_id = album_id;
3425        d.albumart_uri = ContentUris.withAppendedId(mAlbumArtBaseUri, album_id);
3426
3427        // Instead of processing thumbnail requests in the order they were
3428        // received we instead process them stack-based, i.e. LIFO.
3429        // The idea behind this is that the most recently requested thumbnails
3430        // are most likely the ones still in the user's view, whereas those
3431        // requested earlier may have already scrolled off.
3432        synchronized (mThumbRequestStack) {
3433            mThumbRequestStack.push(d);
3434        }
3435
3436        // Trigger the handler.
3437        Message msg = mThumbHandler.obtainMessage(ALBUM_THUMB);
3438        msg.sendToTarget();
3439    }
3440
3441    // Extract compressed image data from the audio file itself or, if that fails,
3442    // look for a file "AlbumArt.jpg" in the containing directory.
3443    private static byte[] getCompressedAlbumArt(Context context, String path) {
3444        byte[] compressed = null;
3445
3446        try {
3447            path = mediaToExternalPath(path);
3448            File f = new File(path);
3449            ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f,
3450                    ParcelFileDescriptor.MODE_READ_ONLY);
3451
3452            MediaScanner scanner = new MediaScanner(context);
3453            compressed = scanner.extractAlbumArt(pfd.getFileDescriptor());
3454            pfd.close();
3455
3456            // If no embedded art exists, look for a suitable image file in the
3457            // same directory as the media file, except if that directory is
3458            // is the root directory of the sd card or the download directory.
3459            // We look for, in order of preference:
3460            // 0 AlbumArt.jpg
3461            // 1 AlbumArt*Large.jpg
3462            // 2 Any other jpg image with 'albumart' anywhere in the name
3463            // 3 Any other jpg image
3464            // 4 any other png image
3465            if (compressed == null && path != null) {
3466                int lastSlash = path.lastIndexOf('/');
3467                if (lastSlash > 0) {
3468
3469                    String artPath = path.substring(0, lastSlash);
3470                    String sdroot = mMediaStoragePath;
3471                    String dwndir = Environment.getExternalStoragePublicDirectory(
3472                            Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
3473
3474                    String bestmatch = null;
3475                    synchronized (sFolderArtMap) {
3476                        if (sFolderArtMap.containsKey(artPath)) {
3477                            bestmatch = sFolderArtMap.get(artPath);
3478                        } else if (!artPath.equalsIgnoreCase(sdroot) &&
3479                                !artPath.equalsIgnoreCase(dwndir)) {
3480                            File dir = new File(artPath);
3481                            String [] entrynames = dir.list();
3482                            if (entrynames == null) {
3483                                return null;
3484                            }
3485                            bestmatch = null;
3486                            int matchlevel = 1000;
3487                            for (int i = entrynames.length - 1; i >=0; i--) {
3488                                String entry = entrynames[i].toLowerCase();
3489                                if (entry.equals("albumart.jpg")) {
3490                                    bestmatch = entrynames[i];
3491                                    break;
3492                                } else if (entry.startsWith("albumart")
3493                                        && entry.endsWith("large.jpg")
3494                                        && matchlevel > 1) {
3495                                    bestmatch = entrynames[i];
3496                                    matchlevel = 1;
3497                                } else if (entry.contains("albumart")
3498                                        && entry.endsWith(".jpg")
3499                                        && matchlevel > 2) {
3500                                    bestmatch = entrynames[i];
3501                                    matchlevel = 2;
3502                                } else if (entry.endsWith(".jpg") && matchlevel > 3) {
3503                                    bestmatch = entrynames[i];
3504                                    matchlevel = 3;
3505                                } else if (entry.endsWith(".png") && matchlevel > 4) {
3506                                    bestmatch = entrynames[i];
3507                                    matchlevel = 4;
3508                                }
3509                            }
3510                            // note that this may insert null if no album art was found
3511                            sFolderArtMap.put(artPath, bestmatch);
3512                        }
3513                    }
3514
3515                    if (bestmatch != null) {
3516                        File file = new File(artPath, bestmatch);
3517                        if (file.exists()) {
3518                            compressed = new byte[(int)file.length()];
3519                            FileInputStream stream = null;
3520                            try {
3521                                stream = new FileInputStream(file);
3522                                stream.read(compressed);
3523                            } catch (IOException ex) {
3524                                compressed = null;
3525                            } finally {
3526                                if (stream != null) {
3527                                    stream.close();
3528                                }
3529                            }
3530                        }
3531                    }
3532                }
3533            }
3534        } catch (IOException e) {
3535        }
3536
3537        return compressed;
3538    }
3539
3540    // Return a URI to write the album art to and update the database as necessary.
3541    Uri getAlbumArtOutputUri(SQLiteDatabase db, long album_id, Uri albumart_uri) {
3542        Uri out = null;
3543        // TODO: this could be done more efficiently with a call to db.replace(), which
3544        // replaces or inserts as needed, making it unnecessary to query() first.
3545        if (albumart_uri != null) {
3546            Cursor c = query(albumart_uri, new String [] { "_data" },
3547                    null, null, null);
3548            try {
3549                if (c != null && c.moveToFirst()) {
3550                    String albumart_path = c.getString(0);
3551                    if (ensureFileExists(albumart_path)) {
3552                        out = albumart_uri;
3553                    }
3554                } else {
3555                    albumart_uri = null;
3556                }
3557            } finally {
3558                if (c != null) {
3559                    c.close();
3560                }
3561            }
3562        }
3563        if (albumart_uri == null){
3564            ContentValues initialValues = new ContentValues();
3565            initialValues.put("album_id", album_id);
3566            try {
3567                ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER);
3568                long rowId = db.insert("album_art", "_data", values);
3569                if (rowId > 0) {
3570                    out = ContentUris.withAppendedId(ALBUMART_URI, rowId);
3571                }
3572            } catch (IllegalStateException ex) {
3573                Log.e(TAG, "error creating album thumb file");
3574            }
3575        }
3576        return out;
3577    }
3578
3579    // Write out the album art to the output URI, recompresses the given Bitmap
3580    // if necessary, otherwise writes the compressed data.
3581    private void writeAlbumArt(
3582            boolean need_to_recompress, Uri out, byte[] compressed, Bitmap bm) {
3583        boolean success = false;
3584        try {
3585            OutputStream outstream = getContext().getContentResolver().openOutputStream(out);
3586
3587            if (!need_to_recompress) {
3588                // No need to recompress here, just write out the original
3589                // compressed data here.
3590                outstream.write(compressed);
3591                success = true;
3592            } else {
3593                success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream);
3594            }
3595
3596            outstream.close();
3597        } catch (FileNotFoundException ex) {
3598            Log.e(TAG, "error creating file", ex);
3599        } catch (IOException ex) {
3600            Log.e(TAG, "error creating file", ex);
3601        }
3602        if (!success) {
3603            // the thumbnail was not written successfully, delete the entry that refers to it
3604            getContext().getContentResolver().delete(out, null, null);
3605        }
3606    }
3607
3608    private ParcelFileDescriptor getThumb(SQLiteDatabase db, String path, long album_id,
3609            Uri albumart_uri) {
3610        ThumbData d = new ThumbData();
3611        d.db = db;
3612        d.path = path;
3613        d.album_id = album_id;
3614        d.albumart_uri = albumart_uri;
3615        return makeThumbInternal(d);
3616    }
3617
3618    private ParcelFileDescriptor makeThumbInternal(ThumbData d) {
3619        byte[] compressed = getCompressedAlbumArt(getContext(), d.path);
3620
3621        if (compressed == null) {
3622            return null;
3623        }
3624
3625        Bitmap bm = null;
3626        boolean need_to_recompress = true;
3627
3628        try {
3629            // get the size of the bitmap
3630            BitmapFactory.Options opts = new BitmapFactory.Options();
3631            opts.inJustDecodeBounds = true;
3632            opts.inSampleSize = 1;
3633            BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts);
3634
3635            // request a reasonably sized output image
3636            // TODO: don't hardcode the size
3637            while (opts.outHeight > 320 || opts.outWidth > 320) {
3638                opts.outHeight /= 2;
3639                opts.outWidth /= 2;
3640                opts.inSampleSize *= 2;
3641            }
3642
3643            if (opts.inSampleSize == 1) {
3644                // The original album art was of proper size, we won't have to
3645                // recompress the bitmap later.
3646                need_to_recompress = false;
3647            } else {
3648                // get the image for real now
3649                opts.inJustDecodeBounds = false;
3650                opts.inPreferredConfig = Bitmap.Config.RGB_565;
3651                bm = BitmapFactory.decodeByteArray(compressed, 0, compressed.length, opts);
3652
3653                if (bm != null && bm.getConfig() == null) {
3654                    Bitmap nbm = bm.copy(Bitmap.Config.RGB_565, false);
3655                    if (nbm != null && nbm != bm) {
3656                        bm.recycle();
3657                        bm = nbm;
3658                    }
3659                }
3660            }
3661        } catch (Exception e) {
3662        }
3663
3664        if (need_to_recompress && bm == null) {
3665            return null;
3666        }
3667
3668        if (d.albumart_uri == null) {
3669            // this one doesn't need to be saved (probably a song with an unknown album),
3670            // so stick it in a memory file and return that
3671            try {
3672                return ParcelFileDescriptor.fromData(compressed, "albumthumb");
3673            } catch (IOException e) {
3674            }
3675        } else {
3676            // This one needs to actually be saved on the sd card.
3677            // This is wrapped in a transaction because there are various things
3678            // that could go wrong while generating the thumbnail, and we only want
3679            // to update the database when all steps succeeded.
3680            d.db.beginTransaction();
3681            try {
3682                Uri out = getAlbumArtOutputUri(d.db, d.album_id, d.albumart_uri);
3683
3684                if (out != null) {
3685                    writeAlbumArt(need_to_recompress, out, compressed, bm);
3686                    getContext().getContentResolver().notifyChange(MEDIA_URI, null);
3687                    ParcelFileDescriptor pfd = openFileHelper(out, "r");
3688                    d.db.setTransactionSuccessful();
3689                    return pfd;
3690                }
3691            } catch (FileNotFoundException ex) {
3692                // do nothing, just return null below
3693            } catch (UnsupportedOperationException ex) {
3694                // do nothing, just return null below
3695            } finally {
3696                d.db.endTransaction();
3697                if (bm != null) {
3698                    bm.recycle();
3699                }
3700            }
3701        }
3702        return null;
3703    }
3704
3705    /**
3706     * Look up the artist or album entry for the given name, creating that entry
3707     * if it does not already exists.
3708     * @param db        The database
3709     * @param table     The table to store the key/name pair in.
3710     * @param keyField  The name of the key-column
3711     * @param nameField The name of the name-column
3712     * @param rawName   The name that the calling app was trying to insert into the database
3713     * @param cacheName The string that will be inserted in to the cache
3714     * @param path      The full path to the file being inserted in to the audio table
3715     * @param albumHash A hash to distinguish between different albums of the same name
3716     * @param artist    The name of the artist, if known
3717     * @param cache     The cache to add this entry to
3718     * @param srcuri    The Uri that prompted the call to this method, used for determining whether this is
3719     *                  the internal or external database
3720     * @return          The row ID for this artist/album, or -1 if the provided name was invalid
3721     */
3722    private long getKeyIdForName(SQLiteDatabase db, String table, String keyField, String nameField,
3723            String rawName, String cacheName, String path, int albumHash,
3724            String artist, HashMap<String, Long> cache, Uri srcuri) {
3725        long rowId;
3726
3727        if (rawName == null || rawName.length() == 0) {
3728            return -1;
3729        }
3730        String k = MediaStore.Audio.keyFor(rawName);
3731
3732        if (k == null) {
3733            return -1;
3734        }
3735
3736        boolean isAlbum = table.equals("albums");
3737        boolean isUnknown = MediaStore.UNKNOWN_STRING.equals(rawName);
3738
3739        // To distinguish same-named albums, we append a hash of the path.
3740        // Ideally we would also take things like CDDB ID in to account, so
3741        // we can group files from the same album that aren't in the same
3742        // folder, but this is a quick and easy start that works immediately
3743        // without requiring support from the mp3, mp4 and Ogg meta data
3744        // readers, as long as the albums are in different folders.
3745        if (isAlbum) {
3746            k = k + albumHash;
3747            if (isUnknown) {
3748                k = k + artist;
3749            }
3750        }
3751
3752        String [] selargs = { k };
3753        Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null);
3754
3755        try {
3756            switch (c.getCount()) {
3757                case 0: {
3758                        // insert new entry into table
3759                        ContentValues otherValues = new ContentValues();
3760                        otherValues.put(keyField, k);
3761                        otherValues.put(nameField, rawName);
3762                        rowId = db.insert(table, "duration", otherValues);
3763                        if (path != null && isAlbum && ! isUnknown) {
3764                            // We just inserted a new album. Now create an album art thumbnail for it.
3765                            makeThumbAsync(db, path, rowId);
3766                        }
3767                        if (rowId > 0) {
3768                            String volume = srcuri.toString().substring(16, 24); // extract internal/external
3769                            Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId);
3770                            getContext().getContentResolver().notifyChange(uri, null);
3771                        }
3772                    }
3773                    break;
3774                case 1: {
3775                        // Use the existing entry
3776                        c.moveToFirst();
3777                        rowId = c.getLong(0);
3778
3779                        // Determine whether the current rawName is better than what's
3780                        // currently stored in the table, and update the table if it is.
3781                        String currentFancyName = c.getString(2);
3782                        String bestName = makeBestName(rawName, currentFancyName);
3783                        if (!bestName.equals(currentFancyName)) {
3784                            // update the table with the new name
3785                            ContentValues newValues = new ContentValues();
3786                            newValues.put(nameField, bestName);
3787                            db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null);
3788                            String volume = srcuri.toString().substring(16, 24); // extract internal/external
3789                            Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId);
3790                            getContext().getContentResolver().notifyChange(uri, null);
3791                        }
3792                    }
3793                    break;
3794                default:
3795                    // corrupt database
3796                    Log.e(TAG, "Multiple entries in table " + table + " for key " + k);
3797                    rowId = -1;
3798                    break;
3799            }
3800        } finally {
3801            if (c != null) c.close();
3802        }
3803
3804        if (cache != null && ! isUnknown) {
3805            cache.put(cacheName, rowId);
3806        }
3807        return rowId;
3808    }
3809
3810    /**
3811     * Returns the best string to use for display, given two names.
3812     * Note that this function does not necessarily return either one
3813     * of the provided names; it may decide to return a better alternative
3814     * (for example, specifying the inputs "Police" and "Police, The" will
3815     * return "The Police")
3816     *
3817     * The basic assumptions are:
3818     * - longer is better ("The police" is better than "Police")
3819     * - prefix is better ("The Police" is better than "Police, The")
3820     * - accents are better ("Mot&ouml;rhead" is better than "Motorhead")
3821     *
3822     * @param one The first of the two names to consider
3823     * @param two The last of the two names to consider
3824     * @return The actual name to use
3825     */
3826    String makeBestName(String one, String two) {
3827        String name;
3828
3829        // Longer names are usually better.
3830        if (one.length() > two.length()) {
3831            name = one;
3832        } else {
3833            // Names with accents are usually better, and conveniently sort later
3834            if (one.toLowerCase().compareTo(two.toLowerCase()) > 0) {
3835                name = one;
3836            } else {
3837                name = two;
3838            }
3839        }
3840
3841        // Prefixes are better than postfixes.
3842        if (name.endsWith(", the") || name.endsWith(",the") ||
3843            name.endsWith(", an") || name.endsWith(",an") ||
3844            name.endsWith(", a") || name.endsWith(",a")) {
3845            String fix = name.substring(1 + name.lastIndexOf(','));
3846            name = fix.trim() + " " + name.substring(0, name.lastIndexOf(','));
3847        }
3848
3849        // TODO: word-capitalize the resulting name
3850        return name;
3851    }
3852
3853
3854    /**
3855     * Looks up the database based on the given URI.
3856     *
3857     * @param uri The requested URI
3858     * @returns the database for the given URI
3859     */
3860    private DatabaseHelper getDatabaseForUri(Uri uri) {
3861        synchronized (mDatabases) {
3862            if (uri.getPathSegments().size() > 1) {
3863                return mDatabases.get(uri.getPathSegments().get(0));
3864            }
3865        }
3866        return null;
3867    }
3868
3869    /**
3870     * Attach the database for a volume (internal or external).
3871     * Does nothing if the volume is already attached, otherwise
3872     * checks the volume ID and sets up the corresponding database.
3873     *
3874     * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}.
3875     * @return the content URI of the attached volume.
3876     */
3877    private Uri attachVolume(String volume) {
3878        if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) {
3879            throw new SecurityException(
3880                    "Opening and closing databases not allowed.");
3881        }
3882
3883        synchronized (mDatabases) {
3884            if (mDatabases.get(volume) != null) {  // Already attached
3885                return Uri.parse("content://media/" + volume);
3886            }
3887
3888            DatabaseHelper db;
3889            if (INTERNAL_VOLUME.equals(volume)) {
3890                db = new DatabaseHelper(getContext(), INTERNAL_DATABASE_NAME, true);
3891            } else if (EXTERNAL_VOLUME.equals(volume)) {
3892                String path = mMediaStoragePath;
3893                int volumeID = FileUtils.getFatVolumeId(path);
3894                if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID);
3895
3896                // generate database name based on volume ID
3897                String dbName = "external-" + Integer.toHexString(volumeID) + ".db";
3898                db = new DatabaseHelper(getContext(), dbName, false);
3899                mVolumeId = volumeID;
3900            } else {
3901                throw new IllegalArgumentException("There is no volume named " + volume);
3902            }
3903
3904            mDatabases.put(volume, db);
3905
3906            if (!db.mInternal) {
3907                // clean up stray album art files: delete every file not in the database
3908                File[] files = new File(mMediaStoragePath, ALBUM_THUMB_FOLDER).listFiles();
3909                HashSet<String> fileSet = new HashSet();
3910                for (int i = 0; files != null && i < files.length; i++) {
3911                    fileSet.add(files[i].getPath());
3912                }
3913
3914                Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
3915                        new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null);
3916                try {
3917                    while (cursor != null && cursor.moveToNext()) {
3918                        fileSet.remove(cursor.getString(0));
3919                    }
3920                } finally {
3921                    if (cursor != null) cursor.close();
3922                }
3923
3924                Iterator<String> iterator = fileSet.iterator();
3925                while (iterator.hasNext()) {
3926                    String filename = iterator.next();
3927                    if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename);
3928                    new File(filename).delete();
3929                }
3930            }
3931        }
3932
3933        if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume);
3934        return Uri.parse("content://media/" + volume);
3935    }
3936
3937    /**
3938     * Detach the database for a volume (must be external).
3939     * Does nothing if the volume is already detached, otherwise
3940     * closes the database and sends a notification to listeners.
3941     *
3942     * @param uri The content URI of the volume, as returned by {@link #attachVolume}
3943     */
3944    private void detachVolume(Uri uri) {
3945        if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) {
3946            throw new SecurityException(
3947                    "Opening and closing databases not allowed.");
3948        }
3949
3950        String volume = uri.getPathSegments().get(0);
3951        if (INTERNAL_VOLUME.equals(volume)) {
3952            throw new UnsupportedOperationException(
3953                    "Deleting the internal volume is not allowed");
3954        } else if (!EXTERNAL_VOLUME.equals(volume)) {
3955            throw new IllegalArgumentException(
3956                    "There is no volume named " + volume);
3957        }
3958
3959        synchronized (mDatabases) {
3960            DatabaseHelper database = mDatabases.get(volume);
3961            if (database == null) return;
3962
3963            try {
3964                // touch the database file to show it is most recently used
3965                File file = new File(database.getReadableDatabase().getPath());
3966                file.setLastModified(System.currentTimeMillis());
3967            } catch (SQLException e) {
3968                Log.e(TAG, "Can't touch database file", e);
3969            }
3970
3971            mDatabases.remove(volume);
3972            database.close();
3973        }
3974
3975        getContext().getContentResolver().notifyChange(uri, null);
3976        if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume);
3977    }
3978
3979    private static String TAG = "MediaProvider";
3980    private static final boolean LOCAL_LOGV = false;
3981    private static final int DATABASE_VERSION = 300;
3982    private static final String INTERNAL_DATABASE_NAME = "internal.db";
3983
3984    // maximum number of cached external databases to keep
3985    private static final int MAX_EXTERNAL_DATABASES = 3;
3986
3987    // Delete databases that have not been used in two months
3988    // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60)
3989    private static final long OBSOLETE_DATABASE_DB = 5184000000L;
3990
3991    private HashMap<String, DatabaseHelper> mDatabases;
3992
3993    private Handler mThumbHandler;
3994
3995    // name of the volume currently being scanned by the media scanner (or null)
3996    private String mMediaScannerVolume;
3997
3998    // current FAT volume ID
3999    private int mVolumeId;
4000
4001    static final String INTERNAL_VOLUME = "internal";
4002    static final String EXTERNAL_VOLUME = "external";
4003    static final String ALBUM_THUMB_FOLDER = "Android/data/com.android.providers.media/albumthumbs";
4004
4005    // path for writing contents of in memory temp database
4006    private String mTempDatabasePath;
4007
4008    // WARNING: the values of IMAGES_MEDIA, AUDIO_MEDIA, and VIDEO_MEDIA and AUDIO_PLAYLISTS
4009    // are stored in the "files" table, so do not renumber them unless you also add
4010    // a corresponding database upgrade step for it.
4011    private static final int IMAGES_MEDIA = 1;
4012    private static final int IMAGES_MEDIA_ID = 2;
4013    private static final int IMAGES_THUMBNAILS = 3;
4014    private static final int IMAGES_THUMBNAILS_ID = 4;
4015
4016    private static final int AUDIO_MEDIA = 100;
4017    private static final int AUDIO_MEDIA_ID = 101;
4018    private static final int AUDIO_MEDIA_ID_GENRES = 102;
4019    private static final int AUDIO_MEDIA_ID_GENRES_ID = 103;
4020    private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104;
4021    private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105;
4022    private static final int AUDIO_GENRES = 106;
4023    private static final int AUDIO_GENRES_ID = 107;
4024    private static final int AUDIO_GENRES_ID_MEMBERS = 108;
4025    private static final int AUDIO_GENRES_ID_MEMBERS_ID = 109;
4026    private static final int AUDIO_PLAYLISTS = 110;
4027    private static final int AUDIO_PLAYLISTS_ID = 111;
4028    private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112;
4029    private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113;
4030    private static final int AUDIO_ARTISTS = 114;
4031    private static final int AUDIO_ARTISTS_ID = 115;
4032    private static final int AUDIO_ALBUMS = 116;
4033    private static final int AUDIO_ALBUMS_ID = 117;
4034    private static final int AUDIO_ARTISTS_ID_ALBUMS = 118;
4035    private static final int AUDIO_ALBUMART = 119;
4036    private static final int AUDIO_ALBUMART_ID = 120;
4037    private static final int AUDIO_ALBUMART_FILE_ID = 121;
4038
4039    private static final int VIDEO_MEDIA = 200;
4040    private static final int VIDEO_MEDIA_ID = 201;
4041    private static final int VIDEO_THUMBNAILS = 202;
4042    private static final int VIDEO_THUMBNAILS_ID = 203;
4043
4044    private static final int VOLUMES = 300;
4045    private static final int VOLUMES_ID = 301;
4046
4047    private static final int AUDIO_SEARCH_LEGACY = 400;
4048    private static final int AUDIO_SEARCH_BASIC = 401;
4049    private static final int AUDIO_SEARCH_FANCY = 402;
4050
4051    private static final int MEDIA_SCANNER = 500;
4052
4053    private static final int FS_ID = 600;
4054
4055    private static final int FILES = 700;
4056    private static final int FILES_ID = 701;
4057
4058    // Used only by the MTP implementation
4059    private static final int MTP_OBJECTS = 702;
4060    private static final int MTP_OBJECTS_ID = 703;
4061    private static final int MTP_OBJECT_REFERENCES = 704;
4062
4063    private static final UriMatcher URI_MATCHER =
4064            new UriMatcher(UriMatcher.NO_MATCH);
4065
4066    private static final String[] ID_PROJECTION = new String[] {
4067        MediaStore.MediaColumns._ID
4068    };
4069
4070    private static final String[] PATH_PROJECTION = new String[] {
4071        MediaStore.MediaColumns._ID,
4072            MediaStore.MediaColumns.DATA,
4073    };
4074
4075    private static final String[] MIME_TYPE_PROJECTION = new String[] {
4076            MediaStore.MediaColumns._ID, // 0
4077            MediaStore.MediaColumns.MIME_TYPE, // 1
4078    };
4079
4080    private static final String[] READY_FLAG_PROJECTION = new String[] {
4081            MediaStore.MediaColumns._ID,
4082            MediaStore.MediaColumns.DATA,
4083            Images.Media.MINI_THUMB_MAGIC
4084    };
4085
4086    private static final String OBJECT_REFERENCES_QUERY =
4087        "SELECT " + Audio.Playlists.Members.AUDIO_ID + " FROM audio_playlists_map"
4088        + " WHERE " + Audio.Playlists.Members.PLAYLIST_ID + "=?"
4089        + " ORDER BY " + Audio.Playlists.Members.PLAY_ORDER;
4090
4091    static
4092    {
4093        URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA);
4094        URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID);
4095        URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS);
4096        URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID);
4097
4098        URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA);
4099        URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID);
4100        URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES);
4101        URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID);
4102        URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS);
4103        URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID);
4104        URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES);
4105        URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID);
4106        URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS);
4107        URI_MATCHER.addURI("media", "*/audio/genres/#/members/#", AUDIO_GENRES_ID_MEMBERS_ID);
4108        URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS);
4109        URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID);
4110        URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS);
4111        URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID);
4112        URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS);
4113        URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID);
4114        URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS);
4115        URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS);
4116        URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID);
4117        URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART);
4118        URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID);
4119        URI_MATCHER.addURI("media", "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID);
4120
4121        URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA);
4122        URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID);
4123        URI_MATCHER.addURI("media", "*/video/thumbnails", VIDEO_THUMBNAILS);
4124        URI_MATCHER.addURI("media", "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID);
4125
4126        URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER);
4127
4128        URI_MATCHER.addURI("media", "*/fs_id", FS_ID);
4129
4130        URI_MATCHER.addURI("media", "*", VOLUMES_ID);
4131        URI_MATCHER.addURI("media", null, VOLUMES);
4132
4133        // Used by MTP implementation
4134        URI_MATCHER.addURI("media", "*/file", FILES);
4135        URI_MATCHER.addURI("media", "*/file/#", FILES_ID);
4136        URI_MATCHER.addURI("media", "*/object", MTP_OBJECTS);
4137        URI_MATCHER.addURI("media", "*/object/#", MTP_OBJECTS_ID);
4138        URI_MATCHER.addURI("media", "*/object/#/references", MTP_OBJECT_REFERENCES);
4139
4140        /**
4141         * @deprecated use the 'basic' or 'fancy' search Uris instead
4142         */
4143        URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY,
4144                AUDIO_SEARCH_LEGACY);
4145        URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
4146                AUDIO_SEARCH_LEGACY);
4147
4148        // used for search suggestions
4149        URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY,
4150                AUDIO_SEARCH_BASIC);
4151        URI_MATCHER.addURI("media", "*/audio/search/" + SearchManager.SUGGEST_URI_PATH_QUERY +
4152                "/*", AUDIO_SEARCH_BASIC);
4153
4154        // used by the music app's search activity
4155        URI_MATCHER.addURI("media", "*/audio/search/fancy", AUDIO_SEARCH_FANCY);
4156        URI_MATCHER.addURI("media", "*/audio/search/fancy/*", AUDIO_SEARCH_FANCY);
4157    }
4158}
4159