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