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