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