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