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