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