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