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