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