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