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