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