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