OSDN Git Service

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