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