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