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