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