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