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