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