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