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