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