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