MediaScanner.java revision 4935d05eaa306cef88cf0ab13eca386f270409ec
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.ContentValues;
20import android.content.Context;
21import android.content.IContentProvider;
22import android.content.ContentUris;
23import android.database.Cursor;
24import android.database.SQLException;
25import android.graphics.BitmapFactory;
26import android.net.Uri;
27import android.os.Process;
28import android.os.RemoteException;
29import android.os.SystemProperties;
30import android.provider.MediaStore;
31import android.provider.Settings;
32import android.provider.MediaStore.Audio;
33import android.provider.MediaStore.Images;
34import android.provider.MediaStore.Video;
35import android.provider.MediaStore.Audio.Genres;
36import android.provider.MediaStore.Audio.Playlists;
37import android.sax.Element;
38import android.sax.ElementListener;
39import android.sax.RootElement;
40import android.text.TextUtils;
41import android.util.Config;
42import android.util.Log;
43import android.util.Xml;
44
45import org.xml.sax.Attributes;
46import org.xml.sax.ContentHandler;
47import org.xml.sax.SAXException;
48
49import java.io.*;
50import java.util.ArrayList;
51import java.util.HashMap;
52import java.util.HashSet;
53import java.util.Iterator;
54
55/**
56 * Internal service helper that no-one should use directly.
57 *
58 * The way the scan currently works is:
59 * - The Java MediaScannerService creates a MediaScanner (this class), and calls
60 *   MediaScanner.scanDirectories on it.
61 * - scanDirectories() calls the native processDirectory() for each of the specified directories.
62 * - the processDirectory() JNI method wraps the provided mediascanner client in a native
63 *   'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner
64 *   object (which got created when the Java MediaScanner was created).
65 * - native MediaScanner.processDirectory() (currently part of opencore) calls
66 *   doProcessDirectory(), which recurses over the folder, and calls
67 *   native MyMediaScannerClient.scanFile() for every file whose extension matches.
68 * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile,
69 *   which calls doScanFile, which after some setup calls back down to native code, calling
70 *   MediaScanner.processFile().
71 * - MediaScanner.processFile() calls one of several methods, depending on the type of the
72 *   file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA.
73 * - each of these methods gets metadata key/value pairs from the file, and repeatedly
74 *   calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java
75 *   counterparts in this file.
76 * - Java handleStringTag() gathers the key/value pairs that it's interested in.
77 * - once processFile returns and we're back in Java code in doScanFile(), it calls
78 *   Java MyMediaScannerClient.endFile(), which takes all the data that's been
79 *   gathered and inserts an entry in to the database.
80 *
81 * In summary:
82 * Java MediaScannerService calls
83 * Java MediaScanner scanDirectories, which calls
84 * Java MediaScanner processDirectory (native method), which calls
85 * native MediaScanner processDirectory, which calls
86 * native MyMediaScannerClient scanFile, which calls
87 * Java MyMediaScannerClient scanFile, which calls
88 * Java MediaScannerClient doScanFile, which calls
89 * Java MediaScanner processFile (native method), which calls
90 * native MediaScanner processFile, which calls
91 * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls
92 * native MyMediaScanner handleStringTag, which calls
93 * Java MyMediaScanner handleStringTag.
94 * Once MediaScanner processFile returns, an entry is inserted in to the database.
95 *
96 * {@hide}
97 */
98public class MediaScanner
99{
100    static {
101        System.loadLibrary("media_jni");
102        native_init();
103    }
104
105    private final static String TAG = "MediaScanner";
106
107    private static final String[] AUDIO_PROJECTION = new String[] {
108            Audio.Media._ID, // 0
109            Audio.Media.DATA, // 1
110            Audio.Media.DATE_MODIFIED, // 2
111    };
112
113    private static final int ID_AUDIO_COLUMN_INDEX = 0;
114    private static final int PATH_AUDIO_COLUMN_INDEX = 1;
115    private static final int DATE_MODIFIED_AUDIO_COLUMN_INDEX = 2;
116
117    private static final String[] VIDEO_PROJECTION = new String[] {
118            Video.Media._ID, // 0
119            Video.Media.DATA, // 1
120            Video.Media.DATE_MODIFIED, // 2
121    };
122
123    private static final int ID_VIDEO_COLUMN_INDEX = 0;
124    private static final int PATH_VIDEO_COLUMN_INDEX = 1;
125    private static final int DATE_MODIFIED_VIDEO_COLUMN_INDEX = 2;
126
127    private static final String[] IMAGES_PROJECTION = new String[] {
128            Images.Media._ID, // 0
129            Images.Media.DATA, // 1
130            Images.Media.DATE_MODIFIED, // 2
131    };
132
133    private static final int ID_IMAGES_COLUMN_INDEX = 0;
134    private static final int PATH_IMAGES_COLUMN_INDEX = 1;
135    private static final int DATE_MODIFIED_IMAGES_COLUMN_INDEX = 2;
136
137    private static final String[] PLAYLISTS_PROJECTION = new String[] {
138            Audio.Playlists._ID, // 0
139            Audio.Playlists.DATA, // 1
140            Audio.Playlists.DATE_MODIFIED, // 2
141    };
142
143    private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] {
144            Audio.Playlists.Members.PLAYLIST_ID, // 0
145     };
146
147    private static final int ID_PLAYLISTS_COLUMN_INDEX = 0;
148    private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1;
149    private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2;
150
151    private static final String[] GENRE_LOOKUP_PROJECTION = new String[] {
152            Audio.Genres._ID, // 0
153            Audio.Genres.NAME, // 1
154    };
155
156    private static final String RINGTONES_DIR = "/ringtones/";
157    private static final String NOTIFICATIONS_DIR = "/notifications/";
158    private static final String ALARMS_DIR = "/alarms/";
159    private static final String MUSIC_DIR = "/music/";
160    private static final String PODCAST_DIR = "/podcasts/";
161
162    private static final String[] ID3_GENRES = {
163        // ID3v1 Genres
164        "Blues",
165        "Classic Rock",
166        "Country",
167        "Dance",
168        "Disco",
169        "Funk",
170        "Grunge",
171        "Hip-Hop",
172        "Jazz",
173        "Metal",
174        "New Age",
175        "Oldies",
176        "Other",
177        "Pop",
178        "R&B",
179        "Rap",
180        "Reggae",
181        "Rock",
182        "Techno",
183        "Industrial",
184        "Alternative",
185        "Ska",
186        "Death Metal",
187        "Pranks",
188        "Soundtrack",
189        "Euro-Techno",
190        "Ambient",
191        "Trip-Hop",
192        "Vocal",
193        "Jazz+Funk",
194        "Fusion",
195        "Trance",
196        "Classical",
197        "Instrumental",
198        "Acid",
199        "House",
200        "Game",
201        "Sound Clip",
202        "Gospel",
203        "Noise",
204        "AlternRock",
205        "Bass",
206        "Soul",
207        "Punk",
208        "Space",
209        "Meditative",
210        "Instrumental Pop",
211        "Instrumental Rock",
212        "Ethnic",
213        "Gothic",
214        "Darkwave",
215        "Techno-Industrial",
216        "Electronic",
217        "Pop-Folk",
218        "Eurodance",
219        "Dream",
220        "Southern Rock",
221        "Comedy",
222        "Cult",
223        "Gangsta",
224        "Top 40",
225        "Christian Rap",
226        "Pop/Funk",
227        "Jungle",
228        "Native American",
229        "Cabaret",
230        "New Wave",
231        "Psychadelic",
232        "Rave",
233        "Showtunes",
234        "Trailer",
235        "Lo-Fi",
236        "Tribal",
237        "Acid Punk",
238        "Acid Jazz",
239        "Polka",
240        "Retro",
241        "Musical",
242        "Rock & Roll",
243        "Hard Rock",
244        // The following genres are Winamp extensions
245        "Folk",
246        "Folk-Rock",
247        "National Folk",
248        "Swing",
249        "Fast Fusion",
250        "Bebob",
251        "Latin",
252        "Revival",
253        "Celtic",
254        "Bluegrass",
255        "Avantgarde",
256        "Gothic Rock",
257        "Progressive Rock",
258        "Psychedelic Rock",
259        "Symphonic Rock",
260        "Slow Rock",
261        "Big Band",
262        "Chorus",
263        "Easy Listening",
264        "Acoustic",
265        "Humour",
266        "Speech",
267        "Chanson",
268        "Opera",
269        "Chamber Music",
270        "Sonata",
271        "Symphony",
272        "Booty Bass",
273        "Primus",
274        "Porn Groove",
275        "Satire",
276        "Slow Jam",
277        "Club",
278        "Tango",
279        "Samba",
280        "Folklore",
281        "Ballad",
282        "Power Ballad",
283        "Rhythmic Soul",
284        "Freestyle",
285        "Duet",
286        "Punk Rock",
287        "Drum Solo",
288        "A capella",
289        "Euro-House",
290        "Dance Hall"
291    };
292
293    private int mNativeContext;
294    private Context mContext;
295    private IContentProvider mMediaProvider;
296    private Uri mAudioUri;
297    private Uri mVideoUri;
298    private Uri mImagesUri;
299    private Uri mThumbsUri;
300    private Uri mGenresUri;
301    private Uri mPlaylistsUri;
302    private boolean mProcessPlaylists, mProcessGenres;
303
304    // used when scanning the image database so we know whether we have to prune
305    // old thumbnail files
306    private int mOriginalCount;
307    /** Whether the scanner has set a default sound for the ringer ringtone. */
308    private boolean mDefaultRingtoneSet;
309    /** Whether the scanner has set a default sound for the notification ringtone. */
310    private boolean mDefaultNotificationSet;
311    /** Whether the scanner has set a default sound for the alarm ringtone. */
312    private boolean mDefaultAlarmSet;
313    /** The filename for the default sound for the ringer ringtone. */
314    private String mDefaultRingtoneFilename;
315    /** The filename for the default sound for the notification ringtone. */
316    private String mDefaultNotificationFilename;
317    /** The filename for the default sound for the alarm ringtone. */
318    private String mDefaultAlarmAlertFilename;
319    /**
320     * The prefix for system properties that define the default sound for
321     * ringtones. Concatenate the name of the setting from Settings
322     * to get the full system property.
323     */
324    private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config.";
325
326    // set to true if file path comparisons should be case insensitive.
327    // this should be set when scanning files on a case insensitive file system.
328    private boolean mCaseInsensitivePaths;
329
330    private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options();
331
332    private static class FileCacheEntry {
333        Uri mTableUri;
334        long mRowId;
335        String mPath;
336        long mLastModified;
337        boolean mSeenInFileSystem;
338        boolean mLastModifiedChanged;
339
340        FileCacheEntry(Uri tableUri, long rowId, String path, long lastModified) {
341            mTableUri = tableUri;
342            mRowId = rowId;
343            mPath = path;
344            mLastModified = lastModified;
345            mSeenInFileSystem = false;
346            mLastModifiedChanged = false;
347        }
348
349        @Override
350        public String toString() {
351            return mPath;
352        }
353    }
354
355    // hashes file path to FileCacheEntry.
356    // path should be lower case if mCaseInsensitivePaths is true
357    private HashMap<String, FileCacheEntry> mFileCache;
358
359    private ArrayList<FileCacheEntry> mPlayLists;
360    private HashMap<String, Uri> mGenreCache;
361
362
363    public MediaScanner(Context c) {
364        native_setup();
365        mContext = c;
366        mBitmapOptions.inSampleSize = 1;
367        mBitmapOptions.inJustDecodeBounds = true;
368
369        setDefaultRingtoneFileNames();
370    }
371
372    private void setDefaultRingtoneFileNames() {
373        mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
374                + Settings.System.RINGTONE);
375        mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
376                + Settings.System.NOTIFICATION_SOUND);
377        mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
378                + Settings.System.ALARM_ALERT);
379    }
380
381    private MyMediaScannerClient mClient = new MyMediaScannerClient();
382
383    private class MyMediaScannerClient implements MediaScannerClient {
384
385        private String mArtist;
386        private String mAlbumArtist;    // use this if mArtist is missing
387        private String mAlbum;
388        private String mTitle;
389        private String mComposer;
390        private String mGenre;
391        private String mMimeType;
392        private int mFileType;
393        private int mTrack;
394        private int mYear;
395        private int mDuration;
396        private String mPath;
397        private long mLastModified;
398        private long mFileSize;
399
400        public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) {
401
402            // special case certain file names
403            // I use regionMatches() instead of substring() below
404            // to avoid memory allocation
405            int lastSlash = path.lastIndexOf('/');
406            if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
407                // ignore those ._* files created by MacOS
408                if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
409                    return null;
410                }
411
412                // ignore album art files created by Windows Media Player:
413                // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg and AlbumArt_{...}_Small.jpg
414                if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
415                    if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
416                            path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
417                        return null;
418                    }
419                    int length = path.length() - lastSlash - 1;
420                    if ((length == 17 && path.regionMatches(true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
421                            (length == 10 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
422                        return null;
423                    }
424                }
425            }
426
427            mMimeType = null;
428            // try mimeType first, if it is specified
429            if (mimeType != null) {
430                mFileType = MediaFile.getFileTypeForMimeType(mimeType);
431                if (mFileType != 0) {
432                    mMimeType = mimeType;
433                }
434            }
435            mFileSize = fileSize;
436
437            // if mimeType was not specified, compute file type based on file extension.
438            if (mMimeType == null) {
439                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
440                if (mediaFileType != null) {
441                    mFileType = mediaFileType.fileType;
442                    mMimeType = mediaFileType.mimeType;
443                }
444            }
445
446            String key = path;
447            if (mCaseInsensitivePaths) {
448                key = path.toLowerCase();
449            }
450            FileCacheEntry entry = mFileCache.get(key);
451            if (entry == null) {
452                entry = new FileCacheEntry(null, 0, path, 0);
453                mFileCache.put(key, entry);
454            }
455            entry.mSeenInFileSystem = true;
456
457            // add some slack to avoid a rounding error
458            long delta = lastModified - entry.mLastModified;
459            if (delta > 1 || delta < -1) {
460                entry.mLastModified = lastModified;
461                entry.mLastModifiedChanged = true;
462            }
463
464            if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
465                mPlayLists.add(entry);
466                // we don't process playlists in the main scan, so return null
467                return null;
468            }
469
470            // clear all the metadata
471            mArtist = null;
472            mAlbumArtist = null;
473            mAlbum = null;
474            mTitle = null;
475            mComposer = null;
476            mGenre = null;
477            mTrack = 0;
478            mYear = 0;
479            mDuration = 0;
480            mPath = path;
481            mLastModified = lastModified;
482
483            return entry;
484        }
485
486        public void scanFile(String path, long lastModified, long fileSize) {
487            doScanFile(path, null, lastModified, fileSize, false);
488        }
489
490        public void scanFile(String path, String mimeType, long lastModified, long fileSize) {
491            doScanFile(path, mimeType, lastModified, fileSize, false);
492        }
493
494        public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) {
495            Uri result = null;
496//            long t1 = System.currentTimeMillis();
497            try {
498                FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize);
499                // rescan for metadata if file was modified since last scan
500                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
501                    String lowpath = path.toLowerCase();
502                    boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
503                    boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
504                    boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
505                    boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
506                    boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
507                        (!ringtones && !notifications && !alarms && !podcasts);
508
509                    if (mFileType == MediaFile.FILE_TYPE_MP3 ||
510                            mFileType == MediaFile.FILE_TYPE_MP4 ||
511                            mFileType == MediaFile.FILE_TYPE_M4A ||
512                            mFileType == MediaFile.FILE_TYPE_3GPP ||
513                            mFileType == MediaFile.FILE_TYPE_3GPP2 ||
514                            mFileType == MediaFile.FILE_TYPE_OGG ||
515                            mFileType == MediaFile.FILE_TYPE_MID ||
516                            mFileType == MediaFile.FILE_TYPE_WMA) {
517                        // we only extract metadata from MP3, M4A, OGG, MID and WMA files.
518                        // check MP4 files, to determine if they contain only audio.
519                        processFile(path, mimeType, this);
520                    } else if (MediaFile.isImageFileType(mFileType)) {
521                        // we used to compute the width and height but it's not worth it
522                    }
523
524                    result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
525                }
526            } catch (RemoteException e) {
527                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
528            }
529//            long t2 = System.currentTimeMillis();
530//            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
531            return result;
532        }
533
534        private int parseSubstring(String s, int start, int defaultValue) {
535            int length = s.length();
536            if (start == length) return defaultValue;
537
538            char ch = s.charAt(start++);
539            // return defaultValue if we have no integer at all
540            if (ch < '0' || ch > '9') return defaultValue;
541
542            int result = ch - '0';
543            while (start < length) {
544                ch = s.charAt(start++);
545                if (ch < '0' || ch > '9') return result;
546                result = result * 10 + (ch - '0');
547            }
548
549            return result;
550        }
551
552        public void handleStringTag(String name, String value) {
553            if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
554                // Don't trim() here, to preserve the special \001 character
555                // used to force sorting. The media provider will trim() before
556                // inserting the title in to the database.
557                mTitle = value;
558            } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
559                mArtist = value.trim();
560            } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")) {
561                mAlbumArtist = value.trim();
562            } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
563                mAlbum = value.trim();
564            } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
565                mComposer = value.trim();
566            } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) {
567                // handle numeric genres, which PV sometimes encodes like "(20)"
568                if (value.length() > 0) {
569                    int genreCode = -1;
570                    char ch = value.charAt(0);
571                    if (ch == '(') {
572                        genreCode = parseSubstring(value, 1, -1);
573                    } else if (ch >= '0' && ch <= '9') {
574                        genreCode = parseSubstring(value, 0, -1);
575                    }
576                    if (genreCode >= 0 && genreCode < ID3_GENRES.length) {
577                        value = ID3_GENRES[genreCode];
578                    }
579                }
580                mGenre = value;
581            } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
582                mYear = parseSubstring(value, 0, 0);
583            } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
584                // track number might be of the form "2/12"
585                // we just read the number before the slash
586                int num = parseSubstring(value, 0, 0);
587                mTrack = (mTrack / 1000) * 1000 + num;
588            } else if (name.equalsIgnoreCase("discnumber") ||
589                    name.equals("set") || name.startsWith("set;")) {
590                // set number might be of the form "1/3"
591                // we just read the number before the slash
592                int num = parseSubstring(value, 0, 0);
593                mTrack = (num * 1000) + (mTrack % 1000);
594            } else if (name.equalsIgnoreCase("duration")) {
595                mDuration = parseSubstring(value, 0, 0);
596            }
597        }
598
599        public void setMimeType(String mimeType) {
600            mMimeType = mimeType;
601            mFileType = MediaFile.getFileTypeForMimeType(mimeType);
602        }
603
604        /**
605         * Formats the data into a values array suitable for use with the Media
606         * Content Provider.
607         *
608         * @return a map of values
609         */
610        private ContentValues toValues() {
611            ContentValues map = new ContentValues();
612
613            map.put(MediaStore.MediaColumns.DATA, mPath);
614            map.put(MediaStore.MediaColumns.TITLE, mTitle);
615            map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
616            map.put(MediaStore.MediaColumns.SIZE, mFileSize);
617            map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
618
619            if (MediaFile.isVideoFileType(mFileType)) {
620                map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING));
621                map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING));
622                map.put(Video.Media.DURATION, mDuration);
623                // FIXME - add RESOLUTION
624            } else if (MediaFile.isImageFileType(mFileType)) {
625                // FIXME - add DESCRIPTION
626                // map.put(field, value);
627            } else if (MediaFile.isAudioFileType(mFileType)) {
628                map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING));
629                map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING));
630                map.put(Audio.Media.COMPOSER, mComposer);
631                if (mYear != 0) {
632                    map.put(Audio.Media.YEAR, mYear);
633                }
634                map.put(Audio.Media.TRACK, mTrack);
635                map.put(Audio.Media.DURATION, mDuration);
636            }
637            return map;
638        }
639
640        private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,
641                boolean alarms, boolean music, boolean podcasts)
642                throws RemoteException {
643            // update database
644            Uri tableUri;
645            boolean isAudio = MediaFile.isAudioFileType(mFileType);
646            boolean isVideo = MediaFile.isVideoFileType(mFileType);
647            boolean isImage = MediaFile.isImageFileType(mFileType);
648            if (isVideo) {
649                tableUri = mVideoUri;
650            } else if (isImage) {
651                tableUri = mImagesUri;
652            } else if (isAudio) {
653                tableUri = mAudioUri;
654            } else {
655                // don't add file to database if not audio, video or image
656                return null;
657            }
658            entry.mTableUri = tableUri;
659
660             // use album artist if artist is missing
661            if (mArtist == null || mArtist.length() == 0) {
662                mArtist = mAlbumArtist;
663            }
664
665            ContentValues values = toValues();
666            String title = values.getAsString(MediaStore.MediaColumns.TITLE);
667            if (title == null || TextUtils.isEmpty(title.trim())) {
668                title = values.getAsString(MediaStore.MediaColumns.DATA);
669                // extract file name after last slash
670                int lastSlash = title.lastIndexOf('/');
671                if (lastSlash >= 0) {
672                    lastSlash++;
673                    if (lastSlash < title.length()) {
674                        title = title.substring(lastSlash);
675                    }
676                }
677                // truncate the file extension (if any)
678                int lastDot = title.lastIndexOf('.');
679                if (lastDot > 0) {
680                    title = title.substring(0, lastDot);
681                }
682                values.put(MediaStore.MediaColumns.TITLE, title);
683            }
684            if (isAudio) {
685                values.put(Audio.Media.IS_RINGTONE, ringtones);
686                values.put(Audio.Media.IS_NOTIFICATION, notifications);
687                values.put(Audio.Media.IS_ALARM, alarms);
688                values.put(Audio.Media.IS_MUSIC, music);
689                values.put(Audio.Media.IS_PODCAST, podcasts);
690            } else if (mFileType == MediaFile.FILE_TYPE_JPEG) {
691                HashMap<String, String> exifData =
692                        ExifInterface.loadExifData(entry.mPath);
693                if (exifData != null) {
694                    float[] latlng = ExifInterface.getLatLng(exifData);
695                    if (latlng != null) {
696                        values.put(Images.Media.LATITUDE, latlng[0]);
697                        values.put(Images.Media.LONGITUDE, latlng[1]);
698                    }
699                }
700            }
701
702            Uri result = null;
703            long rowId = entry.mRowId;
704            if (rowId == 0) {
705                // new file, insert it
706                result = mMediaProvider.insert(tableUri, values);
707                if (result != null) {
708                    rowId = ContentUris.parseId(result);
709                    entry.mRowId = rowId;
710                }
711            } else {
712                // updated file
713                result = ContentUris.withAppendedId(tableUri, rowId);
714                mMediaProvider.update(result, values, null, null);
715            }
716            if (mProcessGenres && mGenre != null) {
717                String genre = mGenre;
718                Uri uri = mGenreCache.get(genre);
719                if (uri == null) {
720                    Cursor cursor = null;
721                    try {
722                        // see if the genre already exists
723                        cursor = mMediaProvider.query(
724                                mGenresUri,
725                                GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?",
726                                        new String[] { genre }, null);
727                        if (cursor == null || cursor.getCount() == 0) {
728                            // genre does not exist, so create the genre in the genre table
729                            values.clear();
730                            values.put(MediaStore.Audio.Genres.NAME, genre);
731                            uri = mMediaProvider.insert(mGenresUri, values);
732                        } else {
733                            // genre already exists, so compute its Uri
734                            cursor.moveToNext();
735                            uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0));
736                        }
737                        if (uri != null) {
738                            uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY);
739                            mGenreCache.put(genre, uri);
740                        }
741                    } finally {
742                        // release the cursor if it exists
743                        if (cursor != null) {
744                            cursor.close();
745                        }
746                    }
747                }
748
749                if (uri != null) {
750                    // add entry to audio_genre_map
751                    values.clear();
752                    values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId));
753                    mMediaProvider.insert(uri, values);
754                }
755            }
756
757            if (notifications && !mDefaultNotificationSet) {
758                if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
759                        doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
760                    setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
761                    mDefaultNotificationSet = true;
762                }
763            } else if (ringtones && !mDefaultRingtoneSet) {
764                if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
765                        doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
766                    setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
767                    mDefaultRingtoneSet = true;
768                }
769            } else if (alarms && !mDefaultAlarmSet) {
770                if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
771                        doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
772                    setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
773                    mDefaultAlarmSet = true;
774                }
775            }
776
777            return result;
778        }
779
780        private boolean doesPathHaveFilename(String path, String filename) {
781            int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
782            int filenameLength = filename.length();
783            return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
784                    pathFilenameStart + filenameLength == path.length();
785        }
786
787        private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
788
789            String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
790                    settingName);
791
792            if (TextUtils.isEmpty(existingSettingValue)) {
793                // Set the setting to the given URI
794                Settings.System.putString(mContext.getContentResolver(), settingName,
795                        ContentUris.withAppendedId(uri, rowId).toString());
796            }
797        }
798
799    }; // end of anonymous MediaScannerClient instance
800
801    private void prescan(String filePath) throws RemoteException {
802        Cursor c = null;
803        String where = null;
804        String[] selectionArgs = null;
805
806        if (mFileCache == null) {
807            mFileCache = new HashMap<String, FileCacheEntry>();
808        } else {
809            mFileCache.clear();
810        }
811        if (mPlayLists == null) {
812            mPlayLists = new ArrayList<FileCacheEntry>();
813        } else {
814            mPlayLists.clear();
815        }
816
817        // Build the list of files from the content provider
818        try {
819            // Read existing files from the audio table
820            if (filePath != null) {
821                where = MediaStore.Audio.Media.DATA + "=?";
822                selectionArgs = new String[] { filePath };
823            }
824            c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null);
825
826            if (c != null) {
827                try {
828                    while (c.moveToNext()) {
829                        long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX);
830                        String path = c.getString(PATH_AUDIO_COLUMN_INDEX);
831                        long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX);
832
833                        String key = path;
834                        if (mCaseInsensitivePaths) {
835                            key = path.toLowerCase();
836                        }
837                        mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path,
838                                lastModified));
839                    }
840                } finally {
841                    c.close();
842                    c = null;
843                }
844            }
845
846            // Read existing files from the video table
847            if (filePath != null) {
848                where = MediaStore.Video.Media.DATA + "=?";
849            } else {
850                where = null;
851            }
852            c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null);
853
854            if (c != null) {
855                try {
856                    while (c.moveToNext()) {
857                        long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX);
858                        String path = c.getString(PATH_VIDEO_COLUMN_INDEX);
859                        long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX);
860
861                        String key = path;
862                        if (mCaseInsensitivePaths) {
863                            key = path.toLowerCase();
864                        }
865                        mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path,
866                                lastModified));
867                    }
868                } finally {
869                    c.close();
870                    c = null;
871                }
872            }
873
874            // Read existing files from the images table
875            if (filePath != null) {
876                where = MediaStore.Images.Media.DATA + "=?";
877            } else {
878                where = null;
879            }
880            mOriginalCount = 0;
881            c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null);
882
883            if (c != null) {
884                try {
885                    mOriginalCount = c.getCount();
886                    while (c.moveToNext()) {
887                        long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX);
888                        String path = c.getString(PATH_IMAGES_COLUMN_INDEX);
889                       long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX);
890
891                        String key = path;
892                        if (mCaseInsensitivePaths) {
893                            key = path.toLowerCase();
894                        }
895                        mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path,
896                                lastModified));
897                    }
898                } finally {
899                    c.close();
900                    c = null;
901                }
902            }
903
904            if (mProcessPlaylists) {
905                // Read existing files from the playlists table
906                if (filePath != null) {
907                    where = MediaStore.Audio.Playlists.DATA + "=?";
908                } else {
909                    where = null;
910                }
911                c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null);
912
913                if (c != null) {
914                    try {
915                        while (c.moveToNext()) {
916                            String path = c.getString(PATH_IMAGES_COLUMN_INDEX);
917
918                            if (path != null && path.length() > 0) {
919                                long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX);
920                                long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX);
921
922                                String key = path;
923                                if (mCaseInsensitivePaths) {
924                                    key = path.toLowerCase();
925                                }
926                                mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path,
927                                        lastModified));
928                            }
929                        }
930                    } finally {
931                        c.close();
932                        c = null;
933                    }
934                }
935            }
936        }
937        finally {
938            if (c != null) {
939                c.close();
940            }
941        }
942    }
943
944    private boolean inScanDirectory(String path, String[] directories) {
945        for (int i = 0; i < directories.length; i++) {
946            if (path.startsWith(directories[i])) {
947                return true;
948            }
949        }
950        return false;
951    }
952
953    private void pruneDeadThumbnailFiles() {
954        HashSet<String> existingFiles = new HashSet<String>();
955        String directory = "/sdcard/DCIM/.thumbnails";
956        String [] files = (new File(directory)).list();
957        if (files == null)
958            files = new String[0];
959
960        for (int i = 0; i < files.length; i++) {
961            String fullPathString = directory + "/" + files[i];
962            existingFiles.add(fullPathString);
963        }
964
965        try {
966            Cursor c = mMediaProvider.query(
967                    mThumbsUri,
968                    new String [] { "_data" },
969                    null,
970                    null,
971                    null);
972            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
973            if (c != null && c.moveToFirst()) {
974                do {
975                    String fullPathString = c.getString(0);
976                    existingFiles.remove(fullPathString);
977                } while (c.moveToNext());
978            }
979
980            for (String fileToDelete : existingFiles) {
981                if (Config.LOGV)
982                    Log.v(TAG, "fileToDelete is " + fileToDelete);
983                try {
984                    (new File(fileToDelete)).delete();
985                } catch (SecurityException ex) {
986                }
987            }
988
989            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
990            if (c != null) {
991                c.close();
992            }
993        } catch (RemoteException e) {
994            // We will soon be killed...
995        }
996    }
997
998    private void postscan(String[] directories) throws RemoteException {
999        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1000
1001        while (iterator.hasNext()) {
1002            FileCacheEntry entry = iterator.next();
1003            String path = entry.mPath;
1004
1005            // remove database entries for files that no longer exist.
1006            boolean fileMissing = false;
1007
1008            if (!entry.mSeenInFileSystem) {
1009                if (inScanDirectory(path, directories)) {
1010                    // we didn't see this file in the scan directory.
1011                    fileMissing = true;
1012                } else {
1013                    // the file is outside of our scan directory,
1014                    // so we need to check for file existence here.
1015                    File testFile = new File(path);
1016                    if (!testFile.exists()) {
1017                        fileMissing = true;
1018                    }
1019                }
1020            }
1021
1022            if (fileMissing) {
1023                // do not delete missing playlists, since they may have been modified by the user.
1024                // the user can delete them in the media player instead.
1025                // instead, clear the path and lastModified fields in the row
1026                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1027                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1028
1029                if (MediaFile.isPlayListFileType(fileType)) {
1030                    ContentValues values = new ContentValues();
1031                    values.put(MediaStore.Audio.Playlists.DATA, "");
1032                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0);
1033                    mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null);
1034                } else {
1035                    mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null);
1036                    iterator.remove();
1037                }
1038            }
1039        }
1040
1041        // handle playlists last, after we know what media files are on the storage.
1042        if (mProcessPlaylists) {
1043            processPlayLists();
1044        }
1045
1046        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1047            pruneDeadThumbnailFiles();
1048
1049        // allow GC to clean up
1050        mGenreCache = null;
1051        mPlayLists = null;
1052        mFileCache = null;
1053        mMediaProvider = null;
1054    }
1055
1056    private void initialize(String volumeName) {
1057        mMediaProvider = mContext.getContentResolver().acquireProvider("media");
1058
1059        mAudioUri = Audio.Media.getContentUri(volumeName);
1060        mVideoUri = Video.Media.getContentUri(volumeName);
1061        mImagesUri = Images.Media.getContentUri(volumeName);
1062        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
1063
1064        if (!volumeName.equals("internal")) {
1065            // we only support playlists on external media
1066            mProcessPlaylists = true;
1067            mProcessGenres = true;
1068            mGenreCache = new HashMap<String, Uri>();
1069            mGenresUri = Genres.getContentUri(volumeName);
1070            mPlaylistsUri = Playlists.getContentUri(volumeName);
1071            // assuming external storage is FAT (case insensitive), except on the simulator.
1072            if ( Process.supportsProcesses()) {
1073                mCaseInsensitivePaths = true;
1074            }
1075        }
1076    }
1077
1078    public void scanDirectories(String[] directories, String volumeName) {
1079        try {
1080            long start = System.currentTimeMillis();
1081            initialize(volumeName);
1082            prescan(null);
1083            long prescan = System.currentTimeMillis();
1084
1085            for (int i = 0; i < directories.length; i++) {
1086                processDirectory(directories[i], MediaFile.sFileExtensions, mClient);
1087            }
1088            long scan = System.currentTimeMillis();
1089            postscan(directories);
1090            long end = System.currentTimeMillis();
1091
1092            if (Config.LOGD) {
1093                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1094                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1095                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1096                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1097            }
1098        } catch (SQLException e) {
1099            // this might happen if the SD card is removed while the media scanner is running
1100            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1101        } catch (UnsupportedOperationException e) {
1102            // this might happen if the SD card is removed while the media scanner is running
1103            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1104        } catch (RemoteException e) {
1105            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1106        }
1107    }
1108
1109    // this function is used to scan a single file
1110    public Uri scanSingleFile(String path, String volumeName, String mimeType) {
1111        try {
1112            initialize(volumeName);
1113            prescan(path);
1114
1115            File file = new File(path);
1116            // always scan the file, so we can return the content://media Uri for existing files
1117            return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true);
1118        } catch (RemoteException e) {
1119            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1120            return null;
1121        }
1122    }
1123
1124    // returns the number of matching file/directory names, starting from the right
1125    private int matchPaths(String path1, String path2) {
1126        int result = 0;
1127        int end1 = path1.length();
1128        int end2 = path2.length();
1129
1130        while (end1 > 0 && end2 > 0) {
1131            int slash1 = path1.lastIndexOf('/', end1 - 1);
1132            int slash2 = path2.lastIndexOf('/', end2 - 1);
1133            int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1134            int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1135            int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1136            int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1137            if (start1 < 0) start1 = 0; else start1++;
1138            if (start2 < 0) start2 = 0; else start2++;
1139            int length = end1 - start1;
1140            if (end2 - start2 != length) break;
1141            if (path1.regionMatches(true, start1, path2, start2, length)) {
1142                result++;
1143                end1 = start1 - 1;
1144                end2 = start2 - 1;
1145            } else break;
1146        }
1147
1148        return result;
1149    }
1150
1151    private boolean addPlayListEntry(String entry, String playListDirectory,
1152            Uri uri, ContentValues values, int index) {
1153
1154        // watch for trailing whitespace
1155        int entryLength = entry.length();
1156        while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--;
1157        // path should be longer than 3 characters.
1158        // avoid index out of bounds errors below by returning here.
1159        if (entryLength < 3) return false;
1160        if (entryLength < entry.length()) entry = entry.substring(0, entryLength);
1161
1162        // does entry appear to be an absolute path?
1163        // look for Unix or DOS absolute paths
1164        char ch1 = entry.charAt(0);
1165        boolean fullPath = (ch1 == '/' ||
1166                (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\'));
1167        // if we have a relative path, combine entry with playListDirectory
1168        if (!fullPath)
1169            entry = playListDirectory + entry;
1170
1171        //FIXME - should we look for "../" within the path?
1172
1173        // best matching MediaFile for the play list entry
1174        FileCacheEntry bestMatch = null;
1175
1176        // number of rightmost file/directory names for bestMatch
1177        int bestMatchLength = 0;
1178
1179        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1180        while (iterator.hasNext()) {
1181            FileCacheEntry cacheEntry = iterator.next();
1182            String path = cacheEntry.mPath;
1183
1184            if (path.equalsIgnoreCase(entry)) {
1185                bestMatch = cacheEntry;
1186                break;    // don't bother continuing search
1187            }
1188
1189            int matchLength = matchPaths(path, entry);
1190            if (matchLength > bestMatchLength) {
1191                bestMatch = cacheEntry;
1192                bestMatchLength = matchLength;
1193            }
1194        }
1195
1196        if (bestMatch == null) {
1197            return false;
1198        }
1199
1200        try {
1201        // OK, now we need to add this to the database
1202            values.clear();
1203            values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1204            values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId));
1205            mMediaProvider.insert(uri, values);
1206        } catch (RemoteException e) {
1207            Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e);
1208            return false;
1209        }
1210
1211        return true;
1212    }
1213
1214    private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1215        BufferedReader reader = null;
1216        try {
1217            File f = new File(path);
1218            if (f.exists()) {
1219                reader = new BufferedReader(
1220                        new InputStreamReader(new FileInputStream(f)), 8192);
1221                String line = reader.readLine();
1222                int index = 0;
1223                while (line != null) {
1224                    // ignore comment lines, which begin with '#'
1225                    if (line.length() > 0 && line.charAt(0) != '#') {
1226                        values.clear();
1227                        if (addPlayListEntry(line, playListDirectory, uri, values, index))
1228                            index++;
1229                    }
1230                    line = reader.readLine();
1231                }
1232            }
1233        } catch (IOException e) {
1234            Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1235        } finally {
1236            try {
1237                if (reader != null)
1238                    reader.close();
1239            } catch (IOException e) {
1240                Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1241            }
1242        }
1243    }
1244
1245    private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1246        BufferedReader reader = null;
1247        try {
1248            File f = new File(path);
1249            if (f.exists()) {
1250                reader = new BufferedReader(
1251                        new InputStreamReader(new FileInputStream(f)), 8192);
1252                String line = reader.readLine();
1253                int index = 0;
1254                while (line != null) {
1255                    // ignore comment lines, which begin with '#'
1256                    if (line.startsWith("File")) {
1257                        int equals = line.indexOf('=');
1258                        if (equals > 0) {
1259                            values.clear();
1260                            if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index))
1261                                index++;
1262                        }
1263                    }
1264                    line = reader.readLine();
1265                }
1266            }
1267        } catch (IOException e) {
1268            Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1269        } finally {
1270            try {
1271                if (reader != null)
1272                    reader.close();
1273            } catch (IOException e) {
1274                Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1275            }
1276        }
1277    }
1278
1279    class WplHandler implements ElementListener {
1280
1281        final ContentHandler handler;
1282        String playListDirectory;
1283        Uri uri;
1284        ContentValues values = new ContentValues();
1285        int index = 0;
1286
1287        public WplHandler(String playListDirectory, Uri uri) {
1288            this.playListDirectory = playListDirectory;
1289            this.uri = uri;
1290
1291            RootElement root = new RootElement("smil");
1292            Element body = root.getChild("body");
1293            Element seq = body.getChild("seq");
1294            Element media = seq.getChild("media");
1295            media.setElementListener(this);
1296
1297            this.handler = root.getContentHandler();
1298        }
1299
1300        public void start(Attributes attributes) {
1301            String path = attributes.getValue("", "src");
1302            if (path != null) {
1303                values.clear();
1304                if (addPlayListEntry(path, playListDirectory, uri, values, index)) {
1305                    index++;
1306                }
1307            }
1308        }
1309
1310       public void end() {
1311       }
1312
1313        ContentHandler getContentHandler() {
1314            return handler;
1315        }
1316    }
1317
1318    private void processWplPlayList(String path, String playListDirectory, Uri uri) {
1319        FileInputStream fis = null;
1320        try {
1321            File f = new File(path);
1322            if (f.exists()) {
1323                fis = new FileInputStream(f);
1324
1325                Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler());
1326            }
1327        } catch (SAXException e) {
1328            e.printStackTrace();
1329        } catch (IOException e) {
1330            e.printStackTrace();
1331        } finally {
1332            try {
1333                if (fis != null)
1334                    fis.close();
1335            } catch (IOException e) {
1336                Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1337            }
1338        }
1339    }
1340
1341    private void processPlayLists() throws RemoteException {
1342        Iterator<FileCacheEntry> iterator = mPlayLists.iterator();
1343        while (iterator.hasNext()) {
1344            FileCacheEntry entry = iterator.next();
1345            String path = entry.mPath;
1346
1347            // only process playlist files if they are new or have been modified since the last scan
1348            if (entry.mLastModifiedChanged) {
1349                ContentValues values = new ContentValues();
1350                int lastSlash = path.lastIndexOf('/');
1351                if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1352                Uri uri, membersUri;
1353                long rowId = entry.mRowId;
1354                if (rowId == 0) {
1355                    // Create a new playlist
1356
1357                    int lastDot = path.lastIndexOf('.');
1358                    String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot));
1359                    values.put(MediaStore.Audio.Playlists.NAME, name);
1360                    values.put(MediaStore.Audio.Playlists.DATA, path);
1361                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1362                    uri = mMediaProvider.insert(mPlaylistsUri, values);
1363                    rowId = ContentUris.parseId(uri);
1364                    membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1365                } else {
1366                    uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1367
1368                    // update lastModified value of existing playlist
1369                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1370                    mMediaProvider.update(uri, values, null, null);
1371
1372                    // delete members of existing playlist
1373                    membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1374                    mMediaProvider.delete(membersUri, null, null);
1375                }
1376
1377                String playListDirectory = path.substring(0, lastSlash + 1);
1378                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1379                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1380
1381                if (fileType == MediaFile.FILE_TYPE_M3U)
1382                    processM3uPlayList(path, playListDirectory, membersUri, values);
1383                else if (fileType == MediaFile.FILE_TYPE_PLS)
1384                    processPlsPlayList(path, playListDirectory, membersUri, values);
1385                else if (fileType == MediaFile.FILE_TYPE_WPL)
1386                    processWplPlayList(path, playListDirectory, membersUri);
1387
1388                Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null,
1389                        null, null);
1390                try {
1391                    if (cursor == null || cursor.getCount() == 0) {
1392                        Log.d(TAG, "playlist is empty - deleting");
1393                        mMediaProvider.delete(uri, null, null);
1394                    }
1395                } finally {
1396                    if (cursor != null) cursor.close();
1397                }
1398            }
1399        }
1400    }
1401
1402    private native void processDirectory(String path, String extensions, MediaScannerClient client);
1403    private native void processFile(String path, String mimeType, MediaScannerClient client);
1404    public native void setLocale(String locale);
1405
1406    public native byte[] extractAlbumArt(FileDescriptor fd);
1407
1408    private static native final void native_init();
1409    private native final void native_setup();
1410    private native final void native_finalize();
1411    @Override
1412    protected void finalize() {
1413        mContext.getContentResolver().releaseProvider(mMediaProvider);
1414        native_finalize();
1415    }
1416}
1417