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