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