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