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