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