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