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