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