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