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