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