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