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