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