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