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