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