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