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