OSDN Git Service

4c113d946d10601556b0107c1652579ef08fc34d
[android-x86/frameworks-base.git] / media / java / android / media / MediaScanner.java
1 /*
2  * Copyright (C) 2007 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 android.media;
18
19 import android.content.ContentProviderClient;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.SharedPreferences;
25 import android.database.Cursor;
26 import android.database.SQLException;
27 import android.drm.DrmManagerClient;
28 import android.graphics.BitmapFactory;
29 import android.mtp.MtpConstants;
30 import android.net.Uri;
31 import android.os.Build;
32 import android.os.Environment;
33 import android.os.RemoteException;
34 import android.os.SystemProperties;
35 import android.provider.MediaStore;
36 import android.provider.MediaStore.Audio;
37 import android.provider.MediaStore.Audio.Playlists;
38 import android.provider.MediaStore.Files;
39 import android.provider.MediaStore.Files.FileColumns;
40 import android.provider.MediaStore.Images;
41 import android.provider.MediaStore.Video;
42 import android.provider.Settings;
43 import android.provider.Settings.SettingNotFoundException;
44 import android.sax.Element;
45 import android.sax.ElementListener;
46 import android.sax.RootElement;
47 import android.system.ErrnoException;
48 import android.system.Os;
49 import android.text.TextUtils;
50 import android.util.Log;
51 import android.util.Xml;
52
53 import dalvik.system.CloseGuard;
54
55 import org.xml.sax.Attributes;
56 import org.xml.sax.ContentHandler;
57 import org.xml.sax.SAXException;
58
59 import java.io.BufferedReader;
60 import java.io.File;
61 import java.io.FileDescriptor;
62 import java.io.FileInputStream;
63 import java.io.IOException;
64 import java.io.InputStreamReader;
65 import java.text.SimpleDateFormat;
66 import java.text.ParseException;
67 import java.util.ArrayList;
68 import java.util.HashMap;
69 import java.util.HashSet;
70 import java.util.Iterator;
71 import java.util.Locale;
72 import java.util.TimeZone;
73 import java.util.concurrent.atomic.AtomicBoolean;
74
75 /**
76  * Internal service helper that no-one should use directly.
77  *
78  * The way the scan currently works is:
79  * - The Java MediaScannerService creates a MediaScanner (this class), and calls
80  *   MediaScanner.scanDirectories on it.
81  * - scanDirectories() calls the native processDirectory() for each of the specified directories.
82  * - the processDirectory() JNI method wraps the provided mediascanner client in a native
83  *   'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner
84  *   object (which got created when the Java MediaScanner was created).
85  * - native MediaScanner.processDirectory() calls
86  *   doProcessDirectory(), which recurses over the folder, and calls
87  *   native MyMediaScannerClient.scanFile() for every file whose extension matches.
88  * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile,
89  *   which calls doScanFile, which after some setup calls back down to native code, calling
90  *   MediaScanner.processFile().
91  * - MediaScanner.processFile() calls one of several methods, depending on the type of the
92  *   file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA.
93  * - each of these methods gets metadata key/value pairs from the file, and repeatedly
94  *   calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java
95  *   counterparts in this file.
96  * - Java handleStringTag() gathers the key/value pairs that it's interested in.
97  * - once processFile returns and we're back in Java code in doScanFile(), it calls
98  *   Java MyMediaScannerClient.endFile(), which takes all the data that's been
99  *   gathered and inserts an entry in to the database.
100  *
101  * In summary:
102  * Java MediaScannerService calls
103  * Java MediaScanner scanDirectories, which calls
104  * Java MediaScanner processDirectory (native method), which calls
105  * native MediaScanner processDirectory, which calls
106  * native MyMediaScannerClient scanFile, which calls
107  * Java MyMediaScannerClient scanFile, which calls
108  * Java MediaScannerClient doScanFile, which calls
109  * Java MediaScanner processFile (native method), which calls
110  * native MediaScanner processFile, which calls
111  * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls
112  * native MyMediaScanner handleStringTag, which calls
113  * Java MyMediaScanner handleStringTag.
114  * Once MediaScanner processFile returns, an entry is inserted in to the database.
115  *
116  * The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner.
117  *
118  * {@hide}
119  */
120 public class MediaScanner implements AutoCloseable {
121     static {
122         System.loadLibrary("media_jni");
123         native_init();
124     }
125
126     private final static String TAG = "MediaScanner";
127
128     private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
129             Files.FileColumns._ID, // 0
130             Files.FileColumns.DATA, // 1
131             Files.FileColumns.FORMAT, // 2
132             Files.FileColumns.DATE_MODIFIED, // 3
133     };
134
135     private static final String[] ID_PROJECTION = new String[] {
136             Files.FileColumns._ID,
137     };
138
139     private static final int FILES_PRESCAN_ID_COLUMN_INDEX = 0;
140     private static final int FILES_PRESCAN_PATH_COLUMN_INDEX = 1;
141     private static final int FILES_PRESCAN_FORMAT_COLUMN_INDEX = 2;
142     private static final int FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX = 3;
143
144     private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] {
145             Audio.Playlists.Members.PLAYLIST_ID, // 0
146      };
147
148     private static final int ID_PLAYLISTS_COLUMN_INDEX = 0;
149     private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1;
150     private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2;
151
152     private static final String RINGTONES_DIR = "/ringtones/";
153     private static final String NOTIFICATIONS_DIR = "/notifications/";
154     private static final String ALARMS_DIR = "/alarms/";
155     private static final String MUSIC_DIR = "/music/";
156     private static final String PODCAST_DIR = "/podcasts/";
157
158     public static final String SCANNED_BUILD_PREFS_NAME = "MediaScanBuild";
159     public static final String LAST_INTERNAL_SCAN_FINGERPRINT = "lastScanFingerprint";
160     private static final String SYSTEM_SOUNDS_DIR = "/system/media/audio";
161     private static String sLastInternalScanFingerprint;
162
163     private static final String[] ID3_GENRES = {
164         // ID3v1 Genres
165         "Blues",
166         "Classic Rock",
167         "Country",
168         "Dance",
169         "Disco",
170         "Funk",
171         "Grunge",
172         "Hip-Hop",
173         "Jazz",
174         "Metal",
175         "New Age",
176         "Oldies",
177         "Other",
178         "Pop",
179         "R&B",
180         "Rap",
181         "Reggae",
182         "Rock",
183         "Techno",
184         "Industrial",
185         "Alternative",
186         "Ska",
187         "Death Metal",
188         "Pranks",
189         "Soundtrack",
190         "Euro-Techno",
191         "Ambient",
192         "Trip-Hop",
193         "Vocal",
194         "Jazz+Funk",
195         "Fusion",
196         "Trance",
197         "Classical",
198         "Instrumental",
199         "Acid",
200         "House",
201         "Game",
202         "Sound Clip",
203         "Gospel",
204         "Noise",
205         "AlternRock",
206         "Bass",
207         "Soul",
208         "Punk",
209         "Space",
210         "Meditative",
211         "Instrumental Pop",
212         "Instrumental Rock",
213         "Ethnic",
214         "Gothic",
215         "Darkwave",
216         "Techno-Industrial",
217         "Electronic",
218         "Pop-Folk",
219         "Eurodance",
220         "Dream",
221         "Southern Rock",
222         "Comedy",
223         "Cult",
224         "Gangsta",
225         "Top 40",
226         "Christian Rap",
227         "Pop/Funk",
228         "Jungle",
229         "Native American",
230         "Cabaret",
231         "New Wave",
232         "Psychadelic",
233         "Rave",
234         "Showtunes",
235         "Trailer",
236         "Lo-Fi",
237         "Tribal",
238         "Acid Punk",
239         "Acid Jazz",
240         "Polka",
241         "Retro",
242         "Musical",
243         "Rock & Roll",
244         "Hard Rock",
245         // The following genres are Winamp extensions
246         "Folk",
247         "Folk-Rock",
248         "National Folk",
249         "Swing",
250         "Fast Fusion",
251         "Bebob",
252         "Latin",
253         "Revival",
254         "Celtic",
255         "Bluegrass",
256         "Avantgarde",
257         "Gothic Rock",
258         "Progressive Rock",
259         "Psychedelic Rock",
260         "Symphonic Rock",
261         "Slow Rock",
262         "Big Band",
263         "Chorus",
264         "Easy Listening",
265         "Acoustic",
266         "Humour",
267         "Speech",
268         "Chanson",
269         "Opera",
270         "Chamber Music",
271         "Sonata",
272         "Symphony",
273         "Booty Bass",
274         "Primus",
275         "Porn Groove",
276         "Satire",
277         "Slow Jam",
278         "Club",
279         "Tango",
280         "Samba",
281         "Folklore",
282         "Ballad",
283         "Power Ballad",
284         "Rhythmic Soul",
285         "Freestyle",
286         "Duet",
287         "Punk Rock",
288         "Drum Solo",
289         "A capella",
290         "Euro-House",
291         "Dance Hall",
292         // The following ones seem to be fairly widely supported as well
293         "Goa",
294         "Drum & Bass",
295         "Club-House",
296         "Hardcore",
297         "Terror",
298         "Indie",
299         "Britpop",
300         null,
301         "Polsk Punk",
302         "Beat",
303         "Christian Gangsta",
304         "Heavy Metal",
305         "Black Metal",
306         "Crossover",
307         "Contemporary Christian",
308         "Christian Rock",
309         "Merengue",
310         "Salsa",
311         "Thrash Metal",
312         "Anime",
313         "JPop",
314         "Synthpop",
315         // 148 and up don't seem to have been defined yet.
316     };
317
318     private long mNativeContext;
319     private final Context mContext;
320     private final String mPackageName;
321     private final String mVolumeName;
322     private final ContentProviderClient mMediaProvider;
323     private final Uri mAudioUri;
324     private final Uri mVideoUri;
325     private final Uri mImagesUri;
326     private final Uri mPlaylistsUri;
327     private final Uri mFilesUri;
328     private final Uri mFilesUriNoNotify;
329     private final boolean mProcessPlaylists;
330     private final boolean mProcessGenres;
331     private int mMtpObjectHandle;
332
333     private final AtomicBoolean mClosed = new AtomicBoolean();
334     private final CloseGuard mCloseGuard = CloseGuard.get();
335
336     /** whether to use bulk inserts or individual inserts for each item */
337     private static final boolean ENABLE_BULK_INSERTS = true;
338
339     // used when scanning the image database so we know whether we have to prune
340     // old thumbnail files
341     private int mOriginalCount;
342     /** Whether the scanner has set a default sound for the ringer ringtone. */
343     private boolean mDefaultRingtoneSet;
344     /** Whether the scanner has set a default sound for the notification ringtone. */
345     private boolean mDefaultNotificationSet;
346     /** Whether the scanner has set a default sound for the alarm ringtone. */
347     private boolean mDefaultAlarmSet;
348     /** The filename for the default sound for the ringer ringtone. */
349     private String mDefaultRingtoneFilename;
350     /** The filename for the default sound for the notification ringtone. */
351     private String mDefaultNotificationFilename;
352     /** The filename for the default sound for the alarm ringtone. */
353     private String mDefaultAlarmAlertFilename;
354     /**
355      * The prefix for system properties that define the default sound for
356      * ringtones. Concatenate the name of the setting from Settings
357      * to get the full system property.
358      */
359     private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config.";
360
361     private final BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options();
362
363     private static class FileEntry {
364         long mRowId;
365         String mPath;
366         long mLastModified;
367         int mFormat;
368         boolean mLastModifiedChanged;
369
370         FileEntry(long rowId, String path, long lastModified, int format) {
371             mRowId = rowId;
372             mPath = path;
373             mLastModified = lastModified;
374             mFormat = format;
375             mLastModifiedChanged = false;
376         }
377
378         @Override
379         public String toString() {
380             return mPath + " mRowId: " + mRowId;
381         }
382     }
383
384     private static class PlaylistEntry {
385         String path;
386         long bestmatchid;
387         int bestmatchlevel;
388     }
389
390     private final ArrayList<PlaylistEntry> mPlaylistEntries = new ArrayList<>();
391     private final ArrayList<FileEntry> mPlayLists = new ArrayList<>();
392
393     private MediaInserter mMediaInserter;
394
395     private DrmManagerClient mDrmManagerClient = null;
396
397     public MediaScanner(Context c, String volumeName) {
398         native_setup();
399         mContext = c;
400         mPackageName = c.getPackageName();
401         mVolumeName = volumeName;
402
403         mBitmapOptions.inSampleSize = 1;
404         mBitmapOptions.inJustDecodeBounds = true;
405
406         setDefaultRingtoneFileNames();
407
408         mMediaProvider = mContext.getContentResolver()
409                 .acquireContentProviderClient(MediaStore.AUTHORITY);
410
411         if (sLastInternalScanFingerprint == null) {
412             final SharedPreferences scanSettings =
413                     mContext.getSharedPreferences(SCANNED_BUILD_PREFS_NAME, Context.MODE_PRIVATE);
414             sLastInternalScanFingerprint =
415                     scanSettings.getString(LAST_INTERNAL_SCAN_FINGERPRINT, new String());
416         }
417
418         mAudioUri = Audio.Media.getContentUri(volumeName);
419         mVideoUri = Video.Media.getContentUri(volumeName);
420         mImagesUri = Images.Media.getContentUri(volumeName);
421         mFilesUri = Files.getContentUri(volumeName);
422         mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();
423
424         if (!volumeName.equals("internal")) {
425             // we only support playlists on external media
426             mProcessPlaylists = true;
427             mProcessGenres = true;
428             mPlaylistsUri = Playlists.getContentUri(volumeName);
429         } else {
430             mProcessPlaylists = false;
431             mProcessGenres = false;
432             mPlaylistsUri = null;
433         }
434
435         final Locale locale = mContext.getResources().getConfiguration().locale;
436         if (locale != null) {
437             String language = locale.getLanguage();
438             String country = locale.getCountry();
439             if (language != null) {
440                 if (country != null) {
441                     setLocale(language + "_" + country);
442                 } else {
443                     setLocale(language);
444                 }
445             }
446         }
447
448         mCloseGuard.open("close");
449     }
450
451     private void setDefaultRingtoneFileNames() {
452         mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
453                 + Settings.System.RINGTONE);
454         mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
455                 + Settings.System.NOTIFICATION_SOUND);
456         mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
457                 + Settings.System.ALARM_ALERT);
458     }
459
460     private final MyMediaScannerClient mClient = new MyMediaScannerClient();
461
462     private boolean isDrmEnabled() {
463         String prop = SystemProperties.get("drm.service.enabled");
464         return prop != null && prop.equals("true");
465     }
466
467     private class MyMediaScannerClient implements MediaScannerClient {
468
469         private final SimpleDateFormat mDateFormatter;
470
471         private String mArtist;
472         private String mAlbumArtist;    // use this if mArtist is missing
473         private String mAlbum;
474         private String mTitle;
475         private String mComposer;
476         private String mGenre;
477         private String mMimeType;
478         private int mFileType;
479         private int mTrack;
480         private int mYear;
481         private int mDuration;
482         private String mPath;
483         private long mDate;
484         private long mLastModified;
485         private long mFileSize;
486         private String mWriter;
487         private int mCompilation;
488         private boolean mIsDrm;
489         private boolean mNoMedia;   // flag to suppress file from appearing in media tables
490         private int mWidth;
491         private int mHeight;
492
493         public MyMediaScannerClient() {
494             mDateFormatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
495             mDateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
496         }
497
498         public FileEntry beginFile(String path, String mimeType, long lastModified,
499                 long fileSize, boolean isDirectory, boolean noMedia) {
500             mMimeType = mimeType;
501             mFileType = 0;
502             mFileSize = fileSize;
503             mIsDrm = false;
504
505             if (!isDirectory) {
506                 if (!noMedia && isNoMediaFile(path)) {
507                     noMedia = true;
508                 }
509                 mNoMedia = noMedia;
510
511                 // try mimeType first, if it is specified
512                 if (mimeType != null) {
513                     mFileType = MediaFile.getFileTypeForMimeType(mimeType);
514                 }
515
516                 // if mimeType was not specified, compute file type based on file extension.
517                 if (mFileType == 0) {
518                     MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
519                     if (mediaFileType != null) {
520                         mFileType = mediaFileType.fileType;
521                         if (mMimeType == null) {
522                             mMimeType = mediaFileType.mimeType;
523                         }
524                     }
525                 }
526
527                 if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
528                     mFileType = getFileTypeFromDrm(path);
529                 }
530             }
531
532             FileEntry entry = makeEntryFor(path);
533             // add some slack to avoid a rounding error
534             long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
535             boolean wasModified = delta > 1 || delta < -1;
536             if (entry == null || wasModified) {
537                 if (wasModified) {
538                     entry.mLastModified = lastModified;
539                 } else {
540                     entry = new FileEntry(0, path, lastModified,
541                             (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
542                 }
543                 entry.mLastModifiedChanged = true;
544             }
545
546             if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
547                 mPlayLists.add(entry);
548                 // we don't process playlists in the main scan, so return null
549                 return null;
550             }
551
552             // clear all the metadata
553             mArtist = null;
554             mAlbumArtist = null;
555             mAlbum = null;
556             mTitle = null;
557             mComposer = null;
558             mGenre = null;
559             mTrack = 0;
560             mYear = 0;
561             mDuration = 0;
562             mPath = path;
563             mDate = 0;
564             mLastModified = lastModified;
565             mWriter = null;
566             mCompilation = 0;
567             mWidth = 0;
568             mHeight = 0;
569
570             return entry;
571         }
572
573         @Override
574         public void scanFile(String path, long lastModified, long fileSize,
575                 boolean isDirectory, boolean noMedia) {
576             // This is the callback funtion from native codes.
577             // Log.v(TAG, "scanFile: "+path);
578             doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
579         }
580
581         public Uri doScanFile(String path, String mimeType, long lastModified,
582                 long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
583             Uri result = null;
584 //            long t1 = System.currentTimeMillis();
585             try {
586                 FileEntry entry = beginFile(path, mimeType, lastModified,
587                         fileSize, isDirectory, noMedia);
588
589                 if (entry == null) {
590                     return null;
591                 }
592
593                 // if this file was just inserted via mtp, set the rowid to zero
594                 // (even though it already exists in the database), to trigger
595                 // the correct code path for updating its entry
596                 if (mMtpObjectHandle != 0) {
597                     entry.mRowId = 0;
598                 }
599
600                 if (entry.mPath != null) {
601                     if (((!mDefaultNotificationSet &&
602                                 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename))
603                         || (!mDefaultRingtoneSet &&
604                                 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename))
605                         || (!mDefaultAlarmSet &&
606                                 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)))) {
607                         Log.w(TAG, "forcing rescan of " + entry.mPath +
608                                 "since ringtone setting didn't finish");
609                         scanAlways = true;
610                     } else if (isSystemSoundWithMetadata(entry.mPath)
611                             && !Build.FINGERPRINT.equals(sLastInternalScanFingerprint)) {
612                         // file is located on the system partition where the date cannot be trusted:
613                         // rescan if the build fingerprint has changed since the last scan.
614                         Log.i(TAG, "forcing rescan of " + entry.mPath
615                                 + " since build fingerprint changed");
616                         scanAlways = true;
617                     }
618                 }
619
620                 // rescan for metadata if file was modified since last scan
621                 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
622                     if (noMedia) {
623                         result = endFile(entry, false, false, false, false, false);
624                     } else {
625                         String lowpath = path.toLowerCase(Locale.ROOT);
626                         boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
627                         boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
628                         boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
629                         boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
630                         boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
631                             (!ringtones && !notifications && !alarms && !podcasts);
632
633                         boolean isaudio = MediaFile.isAudioFileType(mFileType);
634                         boolean isvideo = MediaFile.isVideoFileType(mFileType);
635                         boolean isimage = MediaFile.isImageFileType(mFileType);
636
637                         if (isaudio || isvideo || isimage) {
638                             path = Environment.maybeTranslateEmulatedPathToInternal(new File(path))
639                                     .getAbsolutePath();
640                         }
641
642                         // we only extract metadata for audio and video files
643                         if (isaudio || isvideo) {
644                             processFile(path, mimeType, this);
645                         }
646
647                         if (isimage) {
648                             processImageFile(path);
649                         }
650
651                         result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
652                     }
653                 }
654             } catch (RemoteException e) {
655                 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
656             }
657 //            long t2 = System.currentTimeMillis();
658 //            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
659             return result;
660         }
661
662         private long parseDate(String date) {
663             try {
664               return mDateFormatter.parse(date).getTime();
665             } catch (ParseException e) {
666               return 0;
667             }
668         }
669
670         private int parseSubstring(String s, int start, int defaultValue) {
671             int length = s.length();
672             if (start == length) return defaultValue;
673
674             char ch = s.charAt(start++);
675             // return defaultValue if we have no integer at all
676             if (ch < '0' || ch > '9') return defaultValue;
677
678             int result = ch - '0';
679             while (start < length) {
680                 ch = s.charAt(start++);
681                 if (ch < '0' || ch > '9') return result;
682                 result = result * 10 + (ch - '0');
683             }
684
685             return result;
686         }
687
688         public void handleStringTag(String name, String value) {
689             if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
690                 // Don't trim() here, to preserve the special \001 character
691                 // used to force sorting. The media provider will trim() before
692                 // inserting the title in to the database.
693                 mTitle = value;
694             } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
695                 mArtist = value.trim();
696             } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
697                     || name.equalsIgnoreCase("band") || name.startsWith("band;")) {
698                 mAlbumArtist = value.trim();
699             } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
700                 mAlbum = value.trim();
701             } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
702                 mComposer = value.trim();
703             } else if (mProcessGenres &&
704                     (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) {
705                 mGenre = getGenreName(value);
706             } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
707                 mYear = parseSubstring(value, 0, 0);
708             } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
709                 // track number might be of the form "2/12"
710                 // we just read the number before the slash
711                 int num = parseSubstring(value, 0, 0);
712                 mTrack = (mTrack / 1000) * 1000 + num;
713             } else if (name.equalsIgnoreCase("discnumber") ||
714                     name.equals("set") || name.startsWith("set;")) {
715                 // set number might be of the form "1/3"
716                 // we just read the number before the slash
717                 int num = parseSubstring(value, 0, 0);
718                 mTrack = (num * 1000) + (mTrack % 1000);
719             } else if (name.equalsIgnoreCase("duration")) {
720                 mDuration = parseSubstring(value, 0, 0);
721             } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
722                 mWriter = value.trim();
723             } else if (name.equalsIgnoreCase("compilation")) {
724                 mCompilation = parseSubstring(value, 0, 0);
725             } else if (name.equalsIgnoreCase("isdrm")) {
726                 mIsDrm = (parseSubstring(value, 0, 0) == 1);
727             } else if (name.equalsIgnoreCase("date")) {
728                 mDate = parseDate(value);
729             } else if (name.equalsIgnoreCase("width")) {
730                 mWidth = parseSubstring(value, 0, 0);
731             } else if (name.equalsIgnoreCase("height")) {
732                 mHeight = parseSubstring(value, 0, 0);
733             } else {
734                 //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")");
735             }
736         }
737
738         private boolean convertGenreCode(String input, String expected) {
739             String output = getGenreName(input);
740             if (output.equals(expected)) {
741                 return true;
742             } else {
743                 Log.d(TAG, "'" + input + "' -> '" + output + "', expected '" + expected + "'");
744                 return false;
745             }
746         }
747         private void testGenreNameConverter() {
748             convertGenreCode("2", "Country");
749             convertGenreCode("(2)", "Country");
750             convertGenreCode("(2", "(2");
751             convertGenreCode("2 Foo", "Country");
752             convertGenreCode("(2) Foo", "Country");
753             convertGenreCode("(2 Foo", "(2 Foo");
754             convertGenreCode("2Foo", "2Foo");
755             convertGenreCode("(2)Foo", "Country");
756             convertGenreCode("200 Foo", "Foo");
757             convertGenreCode("(200) Foo", "Foo");
758             convertGenreCode("200Foo", "200Foo");
759             convertGenreCode("(200)Foo", "Foo");
760             convertGenreCode("200)Foo", "200)Foo");
761             convertGenreCode("200) Foo", "200) Foo");
762         }
763
764         public String getGenreName(String genreTagValue) {
765
766             if (genreTagValue == null) {
767                 return null;
768             }
769             final int length = genreTagValue.length();
770
771             if (length > 0) {
772                 boolean parenthesized = false;
773                 StringBuffer number = new StringBuffer();
774                 int i = 0;
775                 for (; i < length; ++i) {
776                     char c = genreTagValue.charAt(i);
777                     if (i == 0 && c == '(') {
778                         parenthesized = true;
779                     } else if (Character.isDigit(c)) {
780                         number.append(c);
781                     } else {
782                         break;
783                     }
784                 }
785                 char charAfterNumber = i < length ? genreTagValue.charAt(i) : ' ';
786                 if ((parenthesized && charAfterNumber == ')')
787                         || !parenthesized && Character.isWhitespace(charAfterNumber)) {
788                     try {
789                         short genreIndex = Short.parseShort(number.toString());
790                         if (genreIndex >= 0) {
791                             if (genreIndex < ID3_GENRES.length && ID3_GENRES[genreIndex] != null) {
792                                 return ID3_GENRES[genreIndex];
793                             } else if (genreIndex == 0xFF) {
794                                 return null;
795                             } else if (genreIndex < 0xFF && (i + 1) < length) {
796                                 // genre is valid but unknown,
797                                 // if there is a string after the value we take it
798                                 if (parenthesized && charAfterNumber == ')') {
799                                     i++;
800                                 }
801                                 String ret = genreTagValue.substring(i).trim();
802                                 if (ret.length() != 0) {
803                                     return ret;
804                                 }
805                             } else {
806                                 // else return the number, without parentheses
807                                 return number.toString();
808                             }
809                         }
810                     } catch (NumberFormatException e) {
811                     }
812                 }
813             }
814
815             return genreTagValue;
816         }
817
818         private void processImageFile(String path) {
819             try {
820                 mBitmapOptions.outWidth = 0;
821                 mBitmapOptions.outHeight = 0;
822                 BitmapFactory.decodeFile(path, mBitmapOptions);
823                 mWidth = mBitmapOptions.outWidth;
824                 mHeight = mBitmapOptions.outHeight;
825             } catch (Throwable th) {
826                 // ignore;
827             }
828         }
829
830         public void setMimeType(String mimeType) {
831             if ("audio/mp4".equals(mMimeType) &&
832                     mimeType.startsWith("video")) {
833                 // for feature parity with Donut, we force m4a files to keep the
834                 // audio/mp4 mimetype, even if they are really "enhanced podcasts"
835                 // with a video track
836                 return;
837             }
838             mMimeType = mimeType;
839             mFileType = MediaFile.getFileTypeForMimeType(mimeType);
840         }
841
842         /**
843          * Formats the data into a values array suitable for use with the Media
844          * Content Provider.
845          *
846          * @return a map of values
847          */
848         private ContentValues toValues() {
849             ContentValues map = new ContentValues();
850
851             map.put(MediaStore.MediaColumns.DATA, mPath);
852             map.put(MediaStore.MediaColumns.TITLE, mTitle);
853             map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
854             map.put(MediaStore.MediaColumns.SIZE, mFileSize);
855             map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
856             map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm);
857
858             String resolution = null;
859             if (mWidth > 0 && mHeight > 0) {
860                 map.put(MediaStore.MediaColumns.WIDTH, mWidth);
861                 map.put(MediaStore.MediaColumns.HEIGHT, mHeight);
862                 resolution = mWidth + "x" + mHeight;
863             }
864
865             if (!mNoMedia) {
866                 if (MediaFile.isVideoFileType(mFileType)) {
867                     map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0
868                             ? mArtist : MediaStore.UNKNOWN_STRING));
869                     map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0
870                             ? mAlbum : MediaStore.UNKNOWN_STRING));
871                     map.put(Video.Media.DURATION, mDuration);
872                     if (resolution != null) {
873                         map.put(Video.Media.RESOLUTION, resolution);
874                     }
875                     if (mDate > 0) {
876                         map.put(Video.Media.DATE_TAKEN, mDate);
877                     }
878                 } else if (MediaFile.isImageFileType(mFileType)) {
879                     // FIXME - add DESCRIPTION
880                 } else if (MediaFile.isAudioFileType(mFileType)) {
881                     map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
882                             mArtist : MediaStore.UNKNOWN_STRING);
883                     map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
884                             mAlbumArtist.length() > 0) ? mAlbumArtist : null);
885                     map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
886                             mAlbum : MediaStore.UNKNOWN_STRING);
887                     map.put(Audio.Media.COMPOSER, mComposer);
888                     map.put(Audio.Media.GENRE, mGenre);
889                     if (mYear != 0) {
890                         map.put(Audio.Media.YEAR, mYear);
891                     }
892                     map.put(Audio.Media.TRACK, mTrack);
893                     map.put(Audio.Media.DURATION, mDuration);
894                     map.put(Audio.Media.COMPILATION, mCompilation);
895                 }
896             }
897             return map;
898         }
899
900         private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
901                 boolean alarms, boolean music, boolean podcasts)
902                 throws RemoteException {
903             // update database
904
905             // use album artist if artist is missing
906             if (mArtist == null || mArtist.length() == 0) {
907                 mArtist = mAlbumArtist;
908             }
909
910             ContentValues values = toValues();
911             String title = values.getAsString(MediaStore.MediaColumns.TITLE);
912             if (title == null || TextUtils.isEmpty(title.trim())) {
913                 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
914                 values.put(MediaStore.MediaColumns.TITLE, title);
915             }
916             String album = values.getAsString(Audio.Media.ALBUM);
917             if (MediaStore.UNKNOWN_STRING.equals(album)) {
918                 album = values.getAsString(MediaStore.MediaColumns.DATA);
919                 // extract last path segment before file name
920                 int lastSlash = album.lastIndexOf('/');
921                 if (lastSlash >= 0) {
922                     int previousSlash = 0;
923                     while (true) {
924                         int idx = album.indexOf('/', previousSlash + 1);
925                         if (idx < 0 || idx >= lastSlash) {
926                             break;
927                         }
928                         previousSlash = idx;
929                     }
930                     if (previousSlash != 0) {
931                         album = album.substring(previousSlash + 1, lastSlash);
932                         values.put(Audio.Media.ALBUM, album);
933                     }
934                 }
935             }
936             long rowId = entry.mRowId;
937             if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
938                 // Only set these for new entries. For existing entries, they
939                 // may have been modified later, and we want to keep the current
940                 // values so that custom ringtones still show up in the ringtone
941                 // picker.
942                 values.put(Audio.Media.IS_RINGTONE, ringtones);
943                 values.put(Audio.Media.IS_NOTIFICATION, notifications);
944                 values.put(Audio.Media.IS_ALARM, alarms);
945                 values.put(Audio.Media.IS_MUSIC, music);
946                 values.put(Audio.Media.IS_PODCAST, podcasts);
947             } else if ((mFileType == MediaFile.FILE_TYPE_JPEG
948                     || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) {
949                 ExifInterface exif = null;
950                 try {
951                     exif = new ExifInterface(entry.mPath);
952                 } catch (IOException ex) {
953                     // exif is null
954                 }
955                 if (exif != null) {
956                     float[] latlng = new float[2];
957                     if (exif.getLatLong(latlng)) {
958                         values.put(Images.Media.LATITUDE, latlng[0]);
959                         values.put(Images.Media.LONGITUDE, latlng[1]);
960                     }
961
962                     long time = exif.getGpsDateTime();
963                     if (time != -1) {
964                         values.put(Images.Media.DATE_TAKEN, time);
965                     } else {
966                         // If no time zone information is available, we should consider using
967                         // EXIF local time as taken time if the difference between file time
968                         // and EXIF local time is not less than 1 Day, otherwise MediaProvider
969                         // will use file time as taken time.
970                         time = exif.getDateTime();
971                         if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) {
972                             values.put(Images.Media.DATE_TAKEN, time);
973                         }
974                     }
975
976                     int orientation = exif.getAttributeInt(
977                         ExifInterface.TAG_ORIENTATION, -1);
978                     if (orientation != -1) {
979                         // We only recognize a subset of orientation tag values.
980                         int degree;
981                         switch(orientation) {
982                             case ExifInterface.ORIENTATION_ROTATE_90:
983                                 degree = 90;
984                                 break;
985                             case ExifInterface.ORIENTATION_ROTATE_180:
986                                 degree = 180;
987                                 break;
988                             case ExifInterface.ORIENTATION_ROTATE_270:
989                                 degree = 270;
990                                 break;
991                             default:
992                                 degree = 0;
993                                 break;
994                         }
995                         values.put(Images.Media.ORIENTATION, degree);
996                     }
997                 }
998             }
999
1000             Uri tableUri = mFilesUri;
1001             MediaInserter inserter = mMediaInserter;
1002             if (!mNoMedia) {
1003                 if (MediaFile.isVideoFileType(mFileType)) {
1004                     tableUri = mVideoUri;
1005                 } else if (MediaFile.isImageFileType(mFileType)) {
1006                     tableUri = mImagesUri;
1007                 } else if (MediaFile.isAudioFileType(mFileType)) {
1008                     tableUri = mAudioUri;
1009                 }
1010             }
1011             Uri result = null;
1012             boolean needToSetSettings = false;
1013             // Setting a flag in order not to use bulk insert for the file related with
1014             // notifications, ringtones, and alarms, because the rowId of the inserted file is
1015             // needed.
1016             if (notifications && !mDefaultNotificationSet) {
1017                 if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
1018                         doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
1019                     needToSetSettings = true;
1020                 }
1021             } else if (ringtones && !mDefaultRingtoneSet) {
1022                 if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
1023                         doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
1024                     needToSetSettings = true;
1025                 }
1026             } else if (alarms && !mDefaultAlarmSet) {
1027                 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
1028                         doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
1029                     needToSetSettings = true;
1030                 }
1031             }
1032
1033             if (rowId == 0) {
1034                 if (mMtpObjectHandle != 0) {
1035                     values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
1036                 }
1037                 if (tableUri == mFilesUri) {
1038                     int format = entry.mFormat;
1039                     if (format == 0) {
1040                         format = MediaFile.getFormatCode(entry.mPath, mMimeType);
1041                     }
1042                     values.put(Files.FileColumns.FORMAT, format);
1043                 }
1044                 // New file, insert it.
1045                 // Directories need to be inserted before the files they contain, so they
1046                 // get priority when bulk inserting.
1047                 // If the rowId of the inserted file is needed, it gets inserted immediately,
1048                 // bypassing the bulk inserter.
1049                 if (inserter == null || needToSetSettings) {
1050                     if (inserter != null) {
1051                         inserter.flushAll();
1052                     }
1053                     result = mMediaProvider.insert(tableUri, values);
1054                 } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
1055                     inserter.insertwithPriority(tableUri, values);
1056                 } else {
1057                     inserter.insert(tableUri, values);
1058                 }
1059
1060                 if (result != null) {
1061                     rowId = ContentUris.parseId(result);
1062                     entry.mRowId = rowId;
1063                 }
1064             } else {
1065                 // updated file
1066                 result = ContentUris.withAppendedId(tableUri, rowId);
1067                 // path should never change, and we want to avoid replacing mixed cased paths
1068                 // with squashed lower case paths
1069                 values.remove(MediaStore.MediaColumns.DATA);
1070
1071                 int mediaType = 0;
1072                 if (!MediaScanner.isNoMediaPath(entry.mPath)) {
1073                     int fileType = MediaFile.getFileTypeForMimeType(mMimeType);
1074                     if (MediaFile.isAudioFileType(fileType)) {
1075                         mediaType = FileColumns.MEDIA_TYPE_AUDIO;
1076                     } else if (MediaFile.isVideoFileType(fileType)) {
1077                         mediaType = FileColumns.MEDIA_TYPE_VIDEO;
1078                     } else if (MediaFile.isImageFileType(fileType)) {
1079                         mediaType = FileColumns.MEDIA_TYPE_IMAGE;
1080                     } else if (MediaFile.isPlayListFileType(fileType)) {
1081                         mediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
1082                     }
1083                     values.put(FileColumns.MEDIA_TYPE, mediaType);
1084                 }
1085                 mMediaProvider.update(result, values, null, null);
1086             }
1087
1088             if(needToSetSettings) {
1089                 if (notifications) {
1090                     setRingtoneIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
1091                     mDefaultNotificationSet = true;
1092                 } else if (ringtones) {
1093                     setRingtoneIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
1094                     mDefaultRingtoneSet = true;
1095                 } else if (alarms) {
1096                     setRingtoneIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
1097                     mDefaultAlarmSet = true;
1098                 }
1099             }
1100
1101             return result;
1102         }
1103
1104         private boolean doesPathHaveFilename(String path, String filename) {
1105             int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
1106             int filenameLength = filename.length();
1107             return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
1108                     pathFilenameStart + filenameLength == path.length();
1109         }
1110
1111         private void setRingtoneIfNotSet(String settingName, Uri uri, long rowId) {
1112             if (wasRingtoneAlreadySet(settingName)) {
1113                 return;
1114             }
1115
1116             ContentResolver cr = mContext.getContentResolver();
1117             String existingSettingValue = Settings.System.getString(cr, settingName);
1118             if (TextUtils.isEmpty(existingSettingValue)) {
1119                 final Uri settingUri = Settings.System.getUriFor(settingName);
1120                 final Uri ringtoneUri = ContentUris.withAppendedId(uri, rowId);
1121                 RingtoneManager.setActualDefaultRingtoneUri(mContext,
1122                         RingtoneManager.getDefaultType(settingUri), ringtoneUri);
1123             }
1124             Settings.System.putInt(cr, settingSetIndicatorName(settingName), 1);
1125         }
1126
1127         private int getFileTypeFromDrm(String path) {
1128             if (!isDrmEnabled()) {
1129                 return 0;
1130             }
1131
1132             int resultFileType = 0;
1133
1134             if (mDrmManagerClient == null) {
1135                 mDrmManagerClient = new DrmManagerClient(mContext);
1136             }
1137
1138             if (mDrmManagerClient.canHandle(path, null)) {
1139                 mIsDrm = true;
1140                 String drmMimetype = mDrmManagerClient.getOriginalMimeType(path);
1141                 if (drmMimetype != null) {
1142                     mMimeType = drmMimetype;
1143                     resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype);
1144                 }
1145             }
1146             return resultFileType;
1147         }
1148
1149     }; // end of anonymous MediaScannerClient instance
1150
1151     private static boolean isSystemSoundWithMetadata(String path) {
1152         if (path.startsWith(SYSTEM_SOUNDS_DIR + ALARMS_DIR)
1153                 || path.startsWith(SYSTEM_SOUNDS_DIR + RINGTONES_DIR)
1154                 || path.startsWith(SYSTEM_SOUNDS_DIR + NOTIFICATIONS_DIR)) {
1155             return true;
1156         }
1157         return false;
1158     }
1159
1160     private String settingSetIndicatorName(String base) {
1161         return base + "_set";
1162     }
1163
1164     private boolean wasRingtoneAlreadySet(String name) {
1165         ContentResolver cr = mContext.getContentResolver();
1166         String indicatorName = settingSetIndicatorName(name);
1167         try {
1168             return Settings.System.getInt(cr, indicatorName) != 0;
1169         } catch (SettingNotFoundException e) {
1170             return false;
1171         }
1172     }
1173
1174     private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
1175         Cursor c = null;
1176         String where = null;
1177         String[] selectionArgs = null;
1178
1179         mPlayLists.clear();
1180
1181         if (filePath != null) {
1182             // query for only one file
1183             where = MediaStore.Files.FileColumns._ID + ">?" +
1184                 " AND " + Files.FileColumns.DATA + "=?";
1185             selectionArgs = new String[] { "", filePath };
1186         } else {
1187             where = MediaStore.Files.FileColumns._ID + ">?";
1188             selectionArgs = new String[] { "" };
1189         }
1190
1191         mDefaultRingtoneSet = wasRingtoneAlreadySet(Settings.System.RINGTONE);
1192         mDefaultNotificationSet = wasRingtoneAlreadySet(Settings.System.NOTIFICATION_SOUND);
1193         mDefaultAlarmSet = wasRingtoneAlreadySet(Settings.System.ALARM_ALERT);
1194
1195         // Tell the provider to not delete the file.
1196         // If the file is truly gone the delete is unnecessary, and we want to avoid
1197         // accidentally deleting files that are really there (this may happen if the
1198         // filesystem is mounted and unmounted while the scanner is running).
1199         Uri.Builder builder = mFilesUri.buildUpon();
1200         builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
1201         MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build());
1202
1203         // Build the list of files from the content provider
1204         try {
1205             if (prescanFiles) {
1206                 // First read existing files from the files table.
1207                 // Because we'll be deleting entries for missing files as we go,
1208                 // we need to query the database in small batches, to avoid problems
1209                 // with CursorWindow positioning.
1210                 long lastId = Long.MIN_VALUE;
1211                 Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
1212
1213                 while (true) {
1214                     selectionArgs[0] = "" + lastId;
1215                     if (c != null) {
1216                         c.close();
1217                         c = null;
1218                     }
1219                     c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION,
1220                             where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
1221                     if (c == null) {
1222                         break;
1223                     }
1224
1225                     int num = c.getCount();
1226
1227                     if (num == 0) {
1228                         break;
1229                     }
1230                     while (c.moveToNext()) {
1231                         long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1232                         String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
1233                         int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
1234                         long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
1235                         lastId = rowId;
1236
1237                         // Only consider entries with absolute path names.
1238                         // This allows storing URIs in the database without the
1239                         // media scanner removing them.
1240                         if (path != null && path.startsWith("/")) {
1241                             boolean exists = false;
1242                             try {
1243                                 exists = Os.access(path, android.system.OsConstants.F_OK);
1244                             } catch (ErrnoException e1) {
1245                             }
1246                             if (!exists && !MtpConstants.isAbstractObject(format)) {
1247                                 // do not delete missing playlists, since they may have been
1248                                 // modified by the user.
1249                                 // The user can delete them in the media player instead.
1250                                 // instead, clear the path and lastModified fields in the row
1251                                 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1252                                 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1253
1254                                 if (!MediaFile.isPlayListFileType(fileType)) {
1255                                     deleter.delete(rowId);
1256                                     if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
1257                                         deleter.flush();
1258                                         String parent = new File(path).getParent();
1259                                         mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
1260                                     }
1261                                 }
1262                             }
1263                         }
1264                     }
1265                 }
1266             }
1267         }
1268         finally {
1269             if (c != null) {
1270                 c.close();
1271             }
1272             deleter.flush();
1273         }
1274
1275         // compute original size of images
1276         mOriginalCount = 0;
1277         c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null);
1278         if (c != null) {
1279             mOriginalCount = c.getCount();
1280             c.close();
1281         }
1282     }
1283
1284     static class MediaBulkDeleter {
1285         StringBuilder whereClause = new StringBuilder();
1286         ArrayList<String> whereArgs = new ArrayList<String>(100);
1287         final ContentProviderClient mProvider;
1288         final Uri mBaseUri;
1289
1290         public MediaBulkDeleter(ContentProviderClient provider, Uri baseUri) {
1291             mProvider = provider;
1292             mBaseUri = baseUri;
1293         }
1294
1295         public void delete(long id) throws RemoteException {
1296             if (whereClause.length() != 0) {
1297                 whereClause.append(",");
1298             }
1299             whereClause.append("?");
1300             whereArgs.add("" + id);
1301             if (whereArgs.size() > 100) {
1302                 flush();
1303             }
1304         }
1305         public void flush() throws RemoteException {
1306             int size = whereArgs.size();
1307             if (size > 0) {
1308                 String [] foo = new String [size];
1309                 foo = whereArgs.toArray(foo);
1310                 int numrows = mProvider.delete(mBaseUri,
1311                         MediaStore.MediaColumns._ID + " IN (" +
1312                         whereClause.toString() + ")", foo);
1313                 //Log.i("@@@@@@@@@", "rows deleted: " + numrows);
1314                 whereClause.setLength(0);
1315                 whereArgs.clear();
1316             }
1317         }
1318     }
1319
1320     private void postscan(final String[] directories) throws RemoteException {
1321
1322         // handle playlists last, after we know what media files are on the storage.
1323         if (mProcessPlaylists) {
1324             processPlayLists();
1325         }
1326
1327         // allow GC to clean up
1328         mPlayLists.clear();
1329     }
1330
1331     private void releaseResources() {
1332         // release the DrmManagerClient resources
1333         if (mDrmManagerClient != null) {
1334             mDrmManagerClient.close();
1335             mDrmManagerClient = null;
1336         }
1337     }
1338
1339     public void scanDirectories(String[] directories) {
1340         try {
1341             long start = System.currentTimeMillis();
1342             prescan(null, true);
1343             long prescan = System.currentTimeMillis();
1344
1345             if (ENABLE_BULK_INSERTS) {
1346                 // create MediaInserter for bulk inserts
1347                 mMediaInserter = new MediaInserter(mMediaProvider, 500);
1348             }
1349
1350             for (int i = 0; i < directories.length; i++) {
1351                 processDirectory(directories[i], mClient);
1352             }
1353
1354             if (ENABLE_BULK_INSERTS) {
1355                 // flush remaining inserts
1356                 mMediaInserter.flushAll();
1357                 mMediaInserter = null;
1358             }
1359
1360             long scan = System.currentTimeMillis();
1361             postscan(directories);
1362             long end = System.currentTimeMillis();
1363
1364             if (false) {
1365                 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1366                 Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1367                 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1368                 Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1369             }
1370         } catch (SQLException e) {
1371             // this might happen if the SD card is removed while the media scanner is running
1372             Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1373         } catch (UnsupportedOperationException e) {
1374             // this might happen if the SD card is removed while the media scanner is running
1375             Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1376         } catch (RemoteException e) {
1377             Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1378         } finally {
1379             releaseResources();
1380         }
1381     }
1382
1383     // this function is used to scan a single file
1384     public Uri scanSingleFile(String path, String mimeType) {
1385         try {
1386             prescan(path, true);
1387
1388             File file = new File(path);
1389             if (!file.exists()) {
1390                 return null;
1391             }
1392
1393             // lastModified is in milliseconds on Files.
1394             long lastModifiedSeconds = file.lastModified() / 1000;
1395
1396             // always scan the file, so we can return the content://media Uri for existing files
1397             return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
1398                     false, true, MediaScanner.isNoMediaPath(path));
1399         } catch (RemoteException e) {
1400             Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1401             return null;
1402         } finally {
1403             releaseResources();
1404         }
1405     }
1406
1407     private static boolean isNoMediaFile(String path) {
1408         File file = new File(path);
1409         if (file.isDirectory()) return false;
1410
1411         // special case certain file names
1412         // I use regionMatches() instead of substring() below
1413         // to avoid memory allocation
1414         int lastSlash = path.lastIndexOf('/');
1415         if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
1416             // ignore those ._* files created by MacOS
1417             if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
1418                 return true;
1419             }
1420
1421             // ignore album art files created by Windows Media Player:
1422             // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
1423             // and AlbumArt_{...}_Small.jpg
1424             if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
1425                 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
1426                         path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
1427                     return true;
1428                 }
1429                 int length = path.length() - lastSlash - 1;
1430                 if ((length == 17 && path.regionMatches(
1431                         true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
1432                         (length == 10
1433                          && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
1434                     return true;
1435                 }
1436             }
1437         }
1438         return false;
1439     }
1440
1441     private static HashMap<String,String> mNoMediaPaths = new HashMap<String,String>();
1442     private static HashMap<String,String> mMediaPaths = new HashMap<String,String>();
1443
1444     /* MediaProvider calls this when a .nomedia file is added or removed */
1445     public static void clearMediaPathCache(boolean clearMediaPaths, boolean clearNoMediaPaths) {
1446         synchronized (MediaScanner.class) {
1447             if (clearMediaPaths) {
1448                 mMediaPaths.clear();
1449             }
1450             if (clearNoMediaPaths) {
1451                 mNoMediaPaths.clear();
1452             }
1453         }
1454     }
1455
1456     public static boolean isNoMediaPath(String path) {
1457         if (path == null) {
1458             return false;
1459         }
1460         // return true if file or any parent directory has name starting with a dot
1461         if (path.indexOf("/.") >= 0) {
1462             return true;
1463         }
1464
1465         int firstSlash = path.lastIndexOf('/');
1466         if (firstSlash <= 0) {
1467             return false;
1468         }
1469         String parent = path.substring(0,  firstSlash);
1470
1471         synchronized (MediaScanner.class) {
1472             if (mNoMediaPaths.containsKey(parent)) {
1473                 return true;
1474             } else if (!mMediaPaths.containsKey(parent)) {
1475                 // check to see if any parent directories have a ".nomedia" file
1476                 // start from 1 so we don't bother checking in the root directory
1477                 int offset = 1;
1478                 while (offset >= 0) {
1479                     int slashIndex = path.indexOf('/', offset);
1480                     if (slashIndex > offset) {
1481                         slashIndex++; // move past slash
1482                         File file = new File(path.substring(0, slashIndex) + ".nomedia");
1483                         if (file.exists()) {
1484                             // we have a .nomedia in one of the parent directories
1485                             mNoMediaPaths.put(parent, "");
1486                             return true;
1487                         }
1488                     }
1489                     offset = slashIndex;
1490                 }
1491                 mMediaPaths.put(parent, "");
1492             }
1493         }
1494
1495         return isNoMediaFile(path);
1496     }
1497
1498     public void scanMtpFile(String path, int objectHandle, int format) {
1499         MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1500         int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1501         File file = new File(path);
1502         long lastModifiedSeconds = file.lastModified() / 1000;
1503
1504         if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) &&
1505             !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) &&
1506             !MediaFile.isDrmFileType(fileType)) {
1507
1508             // no need to use the media scanner, but we need to update last modified and file size
1509             ContentValues values = new ContentValues();
1510             values.put(Files.FileColumns.SIZE, file.length());
1511             values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds);
1512             try {
1513                 String[] whereArgs = new String[] {  Integer.toString(objectHandle) };
1514                 mMediaProvider.update(Files.getMtpObjectsUri(mVolumeName), values,
1515                         "_id=?", whereArgs);
1516             } catch (RemoteException e) {
1517                 Log.e(TAG, "RemoteException in scanMtpFile", e);
1518             }
1519             return;
1520         }
1521
1522         mMtpObjectHandle = objectHandle;
1523         Cursor fileList = null;
1524         try {
1525             if (MediaFile.isPlayListFileType(fileType)) {
1526                 // build file cache so we can look up tracks in the playlist
1527                 prescan(null, true);
1528
1529                 FileEntry entry = makeEntryFor(path);
1530                 if (entry != null) {
1531                     fileList = mMediaProvider.query(mFilesUri,
1532                             FILES_PRESCAN_PROJECTION, null, null, null, null);
1533                     processPlayList(entry, fileList);
1534                 }
1535             } else {
1536                 // MTP will create a file entry for us so we don't want to do it in prescan
1537                 prescan(path, false);
1538
1539                 // always scan the file, so we can return the content://media Uri for existing files
1540                 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(),
1541                     (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path));
1542             }
1543         } catch (RemoteException e) {
1544             Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1545         } finally {
1546             mMtpObjectHandle = 0;
1547             if (fileList != null) {
1548                 fileList.close();
1549             }
1550             releaseResources();
1551         }
1552     }
1553
1554     FileEntry makeEntryFor(String path) {
1555         String where;
1556         String[] selectionArgs;
1557
1558         Cursor c = null;
1559         try {
1560             where = Files.FileColumns.DATA + "=?";
1561             selectionArgs = new String[] { path };
1562             c = mMediaProvider.query(mFilesUriNoNotify, FILES_PRESCAN_PROJECTION,
1563                     where, selectionArgs, null, null);
1564             if (c.moveToFirst()) {
1565                 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1566                 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
1567                 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
1568                 return new FileEntry(rowId, path, lastModified, format);
1569             }
1570         } catch (RemoteException e) {
1571         } finally {
1572             if (c != null) {
1573                 c.close();
1574             }
1575         }
1576         return null;
1577     }
1578
1579     // returns the number of matching file/directory names, starting from the right
1580     private int matchPaths(String path1, String path2) {
1581         int result = 0;
1582         int end1 = path1.length();
1583         int end2 = path2.length();
1584
1585         while (end1 > 0 && end2 > 0) {
1586             int slash1 = path1.lastIndexOf('/', end1 - 1);
1587             int slash2 = path2.lastIndexOf('/', end2 - 1);
1588             int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1589             int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1590             int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1591             int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1592             if (start1 < 0) start1 = 0; else start1++;
1593             if (start2 < 0) start2 = 0; else start2++;
1594             int length = end1 - start1;
1595             if (end2 - start2 != length) break;
1596             if (path1.regionMatches(true, start1, path2, start2, length)) {
1597                 result++;
1598                 end1 = start1 - 1;
1599                 end2 = start2 - 1;
1600             } else break;
1601         }
1602
1603         return result;
1604     }
1605
1606     private boolean matchEntries(long rowId, String data) {
1607
1608         int len = mPlaylistEntries.size();
1609         boolean done = true;
1610         for (int i = 0; i < len; i++) {
1611             PlaylistEntry entry = mPlaylistEntries.get(i);
1612             if (entry.bestmatchlevel == Integer.MAX_VALUE) {
1613                 continue; // this entry has been matched already
1614             }
1615             done = false;
1616             if (data.equalsIgnoreCase(entry.path)) {
1617                 entry.bestmatchid = rowId;
1618                 entry.bestmatchlevel = Integer.MAX_VALUE;
1619                 continue; // no need for path matching
1620             }
1621
1622             int matchLength = matchPaths(data, entry.path);
1623             if (matchLength > entry.bestmatchlevel) {
1624                 entry.bestmatchid = rowId;
1625                 entry.bestmatchlevel = matchLength;
1626             }
1627         }
1628         return done;
1629     }
1630
1631     private void cachePlaylistEntry(String line, String playListDirectory) {
1632         PlaylistEntry entry = new PlaylistEntry();
1633         // watch for trailing whitespace
1634         int entryLength = line.length();
1635         while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--;
1636         // path should be longer than 3 characters.
1637         // avoid index out of bounds errors below by returning here.
1638         if (entryLength < 3) return;
1639         if (entryLength < line.length()) line = line.substring(0, entryLength);
1640
1641         // does entry appear to be an absolute path?
1642         // look for Unix or DOS absolute paths
1643         char ch1 = line.charAt(0);
1644         boolean fullPath = (ch1 == '/' ||
1645                 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\'));
1646         // if we have a relative path, combine entry with playListDirectory
1647         if (!fullPath)
1648             line = playListDirectory + line;
1649         entry.path = line;
1650         //FIXME - should we look for "../" within the path?
1651
1652         mPlaylistEntries.add(entry);
1653     }
1654
1655     private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) {
1656         fileList.moveToPosition(-1);
1657         while (fileList.moveToNext()) {
1658             long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1659             String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
1660             if (matchEntries(rowId, data)) {
1661                 break;
1662             }
1663         }
1664
1665         int len = mPlaylistEntries.size();
1666         int index = 0;
1667         for (int i = 0; i < len; i++) {
1668             PlaylistEntry entry = mPlaylistEntries.get(i);
1669             if (entry.bestmatchlevel > 0) {
1670                 try {
1671                     values.clear();
1672                     values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1673                     values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid));
1674                     mMediaProvider.insert(playlistUri, values);
1675                     index++;
1676                 } catch (RemoteException e) {
1677                     Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e);
1678                     return;
1679                 }
1680             }
1681         }
1682         mPlaylistEntries.clear();
1683     }
1684
1685     private void processM3uPlayList(String path, String playListDirectory, Uri uri,
1686             ContentValues values, Cursor fileList) {
1687         BufferedReader reader = null;
1688         try {
1689             File f = new File(path);
1690             if (f.exists()) {
1691                 reader = new BufferedReader(
1692                         new InputStreamReader(new FileInputStream(f)), 8192);
1693                 String line = reader.readLine();
1694                 mPlaylistEntries.clear();
1695                 while (line != null) {
1696                     // ignore comment lines, which begin with '#'
1697                     if (line.length() > 0 && line.charAt(0) != '#') {
1698                         cachePlaylistEntry(line, playListDirectory);
1699                     }
1700                     line = reader.readLine();
1701                 }
1702
1703                 processCachedPlaylist(fileList, values, uri);
1704             }
1705         } catch (IOException e) {
1706             Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1707         } finally {
1708             try {
1709                 if (reader != null)
1710                     reader.close();
1711             } catch (IOException e) {
1712                 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1713             }
1714         }
1715     }
1716
1717     private void processPlsPlayList(String path, String playListDirectory, Uri uri,
1718             ContentValues values, Cursor fileList) {
1719         BufferedReader reader = null;
1720         try {
1721             File f = new File(path);
1722             if (f.exists()) {
1723                 reader = new BufferedReader(
1724                         new InputStreamReader(new FileInputStream(f)), 8192);
1725                 String line = reader.readLine();
1726                 mPlaylistEntries.clear();
1727                 while (line != null) {
1728                     // ignore comment lines, which begin with '#'
1729                     if (line.startsWith("File")) {
1730                         int equals = line.indexOf('=');
1731                         if (equals > 0) {
1732                             cachePlaylistEntry(line.substring(equals + 1), playListDirectory);
1733                         }
1734                     }
1735                     line = reader.readLine();
1736                 }
1737
1738                 processCachedPlaylist(fileList, values, uri);
1739             }
1740         } catch (IOException e) {
1741             Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1742         } finally {
1743             try {
1744                 if (reader != null)
1745                     reader.close();
1746             } catch (IOException e) {
1747                 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1748             }
1749         }
1750     }
1751
1752     class WplHandler implements ElementListener {
1753
1754         final ContentHandler handler;
1755         String playListDirectory;
1756
1757         public WplHandler(String playListDirectory, Uri uri, Cursor fileList) {
1758             this.playListDirectory = playListDirectory;
1759
1760             RootElement root = new RootElement("smil");
1761             Element body = root.getChild("body");
1762             Element seq = body.getChild("seq");
1763             Element media = seq.getChild("media");
1764             media.setElementListener(this);
1765
1766             this.handler = root.getContentHandler();
1767         }
1768
1769         @Override
1770         public void start(Attributes attributes) {
1771             String path = attributes.getValue("", "src");
1772             if (path != null) {
1773                 cachePlaylistEntry(path, playListDirectory);
1774             }
1775         }
1776
1777        @Override
1778        public void end() {
1779        }
1780
1781         ContentHandler getContentHandler() {
1782             return handler;
1783         }
1784     }
1785
1786     private void processWplPlayList(String path, String playListDirectory, Uri uri,
1787             ContentValues values, Cursor fileList) {
1788         FileInputStream fis = null;
1789         try {
1790             File f = new File(path);
1791             if (f.exists()) {
1792                 fis = new FileInputStream(f);
1793
1794                 mPlaylistEntries.clear();
1795                 Xml.parse(fis, Xml.findEncodingByName("UTF-8"),
1796                         new WplHandler(playListDirectory, uri, fileList).getContentHandler());
1797
1798                 processCachedPlaylist(fileList, values, uri);
1799             }
1800         } catch (SAXException e) {
1801             e.printStackTrace();
1802         } catch (IOException e) {
1803             e.printStackTrace();
1804         } finally {
1805             try {
1806                 if (fis != null)
1807                     fis.close();
1808             } catch (IOException e) {
1809                 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1810             }
1811         }
1812     }
1813
1814     private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException {
1815         String path = entry.mPath;
1816         ContentValues values = new ContentValues();
1817         int lastSlash = path.lastIndexOf('/');
1818         if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1819         Uri uri, membersUri;
1820         long rowId = entry.mRowId;
1821
1822         // make sure we have a name
1823         String name = values.getAsString(MediaStore.Audio.Playlists.NAME);
1824         if (name == null) {
1825             name = values.getAsString(MediaStore.MediaColumns.TITLE);
1826             if (name == null) {
1827                 // extract name from file name
1828                 int lastDot = path.lastIndexOf('.');
1829                 name = (lastDot < 0 ? path.substring(lastSlash + 1)
1830                         : path.substring(lastSlash + 1, lastDot));
1831             }
1832         }
1833
1834         values.put(MediaStore.Audio.Playlists.NAME, name);
1835         values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1836
1837         if (rowId == 0) {
1838             values.put(MediaStore.Audio.Playlists.DATA, path);
1839             uri = mMediaProvider.insert(mPlaylistsUri, values);
1840             rowId = ContentUris.parseId(uri);
1841             membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1842         } else {
1843             uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1844             mMediaProvider.update(uri, values, null, null);
1845
1846             // delete members of existing playlist
1847             membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1848             mMediaProvider.delete(membersUri, null, null);
1849         }
1850
1851         String playListDirectory = path.substring(0, lastSlash + 1);
1852         MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1853         int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1854
1855         if (fileType == MediaFile.FILE_TYPE_M3U) {
1856             processM3uPlayList(path, playListDirectory, membersUri, values, fileList);
1857         } else if (fileType == MediaFile.FILE_TYPE_PLS) {
1858             processPlsPlayList(path, playListDirectory, membersUri, values, fileList);
1859         } else if (fileType == MediaFile.FILE_TYPE_WPL) {
1860             processWplPlayList(path, playListDirectory, membersUri, values, fileList);
1861         }
1862     }
1863
1864     private void processPlayLists() throws RemoteException {
1865         Iterator<FileEntry> iterator = mPlayLists.iterator();
1866         Cursor fileList = null;
1867         try {
1868             // use the files uri and projection because we need the format column,
1869             // but restrict the query to just audio files
1870             fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION,
1871                     "media_type=2", null, null, null);
1872             while (iterator.hasNext()) {
1873                 FileEntry entry = iterator.next();
1874                 // only process playlist files if they are new or have been modified since the last scan
1875                 if (entry.mLastModifiedChanged) {
1876                     processPlayList(entry, fileList);
1877                 }
1878             }
1879         } catch (RemoteException e1) {
1880         } finally {
1881             if (fileList != null) {
1882                 fileList.close();
1883             }
1884         }
1885     }
1886
1887     private native void processDirectory(String path, MediaScannerClient client);
1888     private native void processFile(String path, String mimeType, MediaScannerClient client);
1889     private native void setLocale(String locale);
1890
1891     public native byte[] extractAlbumArt(FileDescriptor fd);
1892
1893     private static native final void native_init();
1894     private native final void native_setup();
1895     private native final void native_finalize();
1896
1897     @Override
1898     public void close() {
1899         mCloseGuard.close();
1900         if (mClosed.compareAndSet(false, true)) {
1901             mMediaProvider.close();
1902             native_finalize();
1903         }
1904     }
1905
1906     @Override
1907     protected void finalize() throws Throwable {
1908         try {
1909             mCloseGuard.warnIfOpen();
1910             close();
1911         } finally {
1912             super.finalize();
1913         }
1914     }
1915 }