MediaScanner.java revision d4f3586c93cd9c29056e95cd79f3cb19ac456f3b
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        private String mWriter;
400
401        public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) {
402
403            // special case certain file names
404            // I use regionMatches() instead of substring() below
405            // to avoid memory allocation
406            int lastSlash = path.lastIndexOf('/');
407            if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
408                // ignore those ._* files created by MacOS
409                if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
410                    return null;
411                }
412
413                // ignore album art files created by Windows Media Player:
414                // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg and AlbumArt_{...}_Small.jpg
415                if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
416                    if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
417                            path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
418                        return null;
419                    }
420                    int length = path.length() - lastSlash - 1;
421                    if ((length == 17 && path.regionMatches(true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
422                            (length == 10 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
423                        return null;
424                    }
425                }
426            }
427
428            mMimeType = null;
429            // try mimeType first, if it is specified
430            if (mimeType != null) {
431                mFileType = MediaFile.getFileTypeForMimeType(mimeType);
432                if (mFileType != 0) {
433                    mMimeType = mimeType;
434                }
435            }
436            mFileSize = fileSize;
437
438            // if mimeType was not specified, compute file type based on file extension.
439            if (mMimeType == null) {
440                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
441                if (mediaFileType != null) {
442                    mFileType = mediaFileType.fileType;
443                    mMimeType = mediaFileType.mimeType;
444                }
445            }
446
447            String key = path;
448            if (mCaseInsensitivePaths) {
449                key = path.toLowerCase();
450            }
451            FileCacheEntry entry = mFileCache.get(key);
452            if (entry == null) {
453                entry = new FileCacheEntry(null, 0, path, 0);
454                mFileCache.put(key, entry);
455            }
456            entry.mSeenInFileSystem = true;
457
458            // add some slack to avoid a rounding error
459            long delta = lastModified - entry.mLastModified;
460            if (delta > 1 || delta < -1) {
461                entry.mLastModified = lastModified;
462                entry.mLastModifiedChanged = true;
463            }
464
465            if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
466                mPlayLists.add(entry);
467                // we don't process playlists in the main scan, so return null
468                return null;
469            }
470
471            // clear all the metadata
472            mArtist = null;
473            mAlbumArtist = null;
474            mAlbum = null;
475            mTitle = null;
476            mComposer = null;
477            mGenre = null;
478            mTrack = 0;
479            mYear = 0;
480            mDuration = 0;
481            mPath = path;
482            mLastModified = lastModified;
483            mWriter = null;
484
485            return entry;
486        }
487
488        public void scanFile(String path, long lastModified, long fileSize) {
489            // This is the callback funtion from native codes.
490            // Log.v(TAG, "scanFile: "+path);
491            doScanFile(path, null, lastModified, fileSize, false);
492        }
493
494        public void scanFile(String path, String mimeType, long lastModified, long fileSize) {
495            doScanFile(path, mimeType, lastModified, fileSize, false);
496        }
497
498        private boolean isMetadataSupported(int fileType) {
499            if (mFileType == MediaFile.FILE_TYPE_MP3 ||
500                    mFileType == MediaFile.FILE_TYPE_MP4 ||
501                    mFileType == MediaFile.FILE_TYPE_M4A ||
502                    mFileType == MediaFile.FILE_TYPE_3GPP ||
503                    mFileType == MediaFile.FILE_TYPE_3GPP2 ||
504                    mFileType == MediaFile.FILE_TYPE_OGG ||
505                    mFileType == MediaFile.FILE_TYPE_AAC ||
506                    mFileType == MediaFile.FILE_TYPE_MID ||
507                    mFileType == MediaFile.FILE_TYPE_WMA) {
508                // we only extract metadata from MP3, M4A, OGG, MID, AAC and WMA files.
509                // check MP4 files, to determine if they contain only audio.
510                return true;
511            }
512            return false;
513        }
514        public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) {
515            Uri result = null;
516//            long t1 = System.currentTimeMillis();
517            try {
518                FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize);
519                // rescan for metadata if file was modified since last scan
520                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
521                    String lowpath = path.toLowerCase();
522                    boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
523                    boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
524                    boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
525                    boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
526                    boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
527                        (!ringtones && !notifications && !alarms && !podcasts);
528
529                    if( isMetadataSupported(mFileType) ) {
530                        processFile(path, mimeType, this);
531                    } else if (MediaFile.isImageFileType(mFileType)) {
532                        // we used to compute the width and height but it's not worth it
533                    }
534
535                    result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
536                }
537            } catch (RemoteException e) {
538                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
539            }
540//            long t2 = System.currentTimeMillis();
541//            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
542            return result;
543        }
544
545        private int parseSubstring(String s, int start, int defaultValue) {
546            int length = s.length();
547            if (start == length) return defaultValue;
548
549            char ch = s.charAt(start++);
550            // return defaultValue if we have no integer at all
551            if (ch < '0' || ch > '9') return defaultValue;
552
553            int result = ch - '0';
554            while (start < length) {
555                ch = s.charAt(start++);
556                if (ch < '0' || ch > '9') return result;
557                result = result * 10 + (ch - '0');
558            }
559
560            return result;
561        }
562
563        public void handleStringTag(String name, String value) {
564            if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
565                // Don't trim() here, to preserve the special \001 character
566                // used to force sorting. The media provider will trim() before
567                // inserting the title in to the database.
568                mTitle = value;
569            } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
570                mArtist = value.trim();
571            } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")) {
572                mAlbumArtist = value.trim();
573            } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
574                mAlbum = value.trim();
575            } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
576                mComposer = value.trim();
577            } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) {
578                // handle numeric genres, which PV sometimes encodes like "(20)"
579                if (value.length() > 0) {
580                    int genreCode = -1;
581                    char ch = value.charAt(0);
582                    if (ch == '(') {
583                        genreCode = parseSubstring(value, 1, -1);
584                    } else if (ch >= '0' && ch <= '9') {
585                        genreCode = parseSubstring(value, 0, -1);
586                    }
587                    if (genreCode >= 0 && genreCode < ID3_GENRES.length) {
588                        value = ID3_GENRES[genreCode];
589                    }
590                }
591                mGenre = value;
592            } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
593                mYear = parseSubstring(value, 0, 0);
594            } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
595                // track number might be of the form "2/12"
596                // we just read the number before the slash
597                int num = parseSubstring(value, 0, 0);
598                mTrack = (mTrack / 1000) * 1000 + num;
599            } else if (name.equalsIgnoreCase("discnumber") ||
600                    name.equals("set") || name.startsWith("set;")) {
601                // set number might be of the form "1/3"
602                // we just read the number before the slash
603                int num = parseSubstring(value, 0, 0);
604                mTrack = (num * 1000) + (mTrack % 1000);
605            } else if (name.equalsIgnoreCase("duration")) {
606                mDuration = parseSubstring(value, 0, 0);
607            } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
608                mWriter = value.trim();
609            }
610        }
611
612        public void setMimeType(String mimeType) {
613            mMimeType = mimeType;
614            mFileType = MediaFile.getFileTypeForMimeType(mimeType);
615        }
616
617        /**
618         * Formats the data into a values array suitable for use with the Media
619         * Content Provider.
620         *
621         * @return a map of values
622         */
623        private ContentValues toValues() {
624            ContentValues map = new ContentValues();
625
626            map.put(MediaStore.MediaColumns.DATA, mPath);
627            map.put(MediaStore.MediaColumns.TITLE, mTitle);
628            map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
629            map.put(MediaStore.MediaColumns.SIZE, mFileSize);
630            map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
631
632            if (MediaFile.isVideoFileType(mFileType)) {
633                map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING));
634                map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING));
635                map.put(Video.Media.DURATION, mDuration);
636                // FIXME - add RESOLUTION
637            } else if (MediaFile.isImageFileType(mFileType)) {
638                // FIXME - add DESCRIPTION
639                // map.put(field, value);
640            } else if (MediaFile.isAudioFileType(mFileType)) {
641                map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING));
642                map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING));
643                map.put(Audio.Media.COMPOSER, mComposer);
644                if (mYear != 0) {
645                    map.put(Audio.Media.YEAR, mYear);
646                }
647                map.put(Audio.Media.TRACK, mTrack);
648                map.put(Audio.Media.DURATION, mDuration);
649            }
650            return map;
651        }
652
653        private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,
654                boolean alarms, boolean music, boolean podcasts)
655                throws RemoteException {
656            // update database
657            Uri tableUri;
658            boolean isAudio = MediaFile.isAudioFileType(mFileType);
659            boolean isVideo = MediaFile.isVideoFileType(mFileType);
660            boolean isImage = MediaFile.isImageFileType(mFileType);
661            if (isVideo) {
662                tableUri = mVideoUri;
663            } else if (isImage) {
664                tableUri = mImagesUri;
665            } else if (isAudio) {
666                tableUri = mAudioUri;
667            } else {
668                // don't add file to database if not audio, video or image
669                return null;
670            }
671            entry.mTableUri = tableUri;
672
673             // use album artist if artist is missing
674            if (mArtist == null || mArtist.length() == 0) {
675                mArtist = mAlbumArtist;
676            }
677
678            ContentValues values = toValues();
679            String title = values.getAsString(MediaStore.MediaColumns.TITLE);
680            if (title == null || TextUtils.isEmpty(title.trim())) {
681                title = values.getAsString(MediaStore.MediaColumns.DATA);
682                // extract file name after last slash
683                int lastSlash = title.lastIndexOf('/');
684                if (lastSlash >= 0) {
685                    lastSlash++;
686                    if (lastSlash < title.length()) {
687                        title = title.substring(lastSlash);
688                    }
689                }
690                // truncate the file extension (if any)
691                int lastDot = title.lastIndexOf('.');
692                if (lastDot > 0) {
693                    title = title.substring(0, lastDot);
694                }
695                values.put(MediaStore.MediaColumns.TITLE, title);
696            }
697            String album = values.getAsString(Audio.Media.ALBUM);
698            if (MediaFile.UNKNOWN_STRING.equals(album)) {
699                album = values.getAsString(MediaStore.MediaColumns.DATA);
700                // extract last path segment before file name
701                int lastSlash = album.lastIndexOf('/');
702                if (lastSlash >= 0) {
703                    int previousSlash = 0;
704                    while (true) {
705                        int idx = album.indexOf('/', previousSlash + 1);
706                        if (idx < 0 || idx >= lastSlash) {
707                            break;
708                        }
709                        previousSlash = idx;
710                    }
711                    if (previousSlash != 0) {
712                        album = album.substring(previousSlash + 1, lastSlash);
713                        values.put(Audio.Media.ALBUM, album);
714                    }
715                }
716            }
717            if (isAudio) {
718                values.put(Audio.Media.IS_RINGTONE, ringtones);
719                values.put(Audio.Media.IS_NOTIFICATION, notifications);
720                values.put(Audio.Media.IS_ALARM, alarms);
721                values.put(Audio.Media.IS_MUSIC, music);
722                values.put(Audio.Media.IS_PODCAST, podcasts);
723            } else if (mFileType == MediaFile.FILE_TYPE_JPEG) {
724                ExifInterface exif = null;
725                try {
726                    exif = new ExifInterface(entry.mPath);
727                } catch (IOException ex) {
728                    // exif is null
729                }
730                if (exif != null) {
731                    float[] latlng = new float[2];
732                    if (exif.getLatLong(latlng)) {
733                        values.put(Images.Media.LATITUDE, latlng[0]);
734                        values.put(Images.Media.LONGITUDE, latlng[1]);
735                    }
736
737                    long time = exif.getDateTime();
738                    if (time != -1) {
739                        values.put(Images.Media.DATE_TAKEN, time);
740                    }
741
742                    int orientation = exif.getAttributeInt(
743                        ExifInterface.TAG_ORIENTATION, -1);
744                    if (orientation != -1) {
745                        // We only recognize a subset of orientation tag values.
746                        int degree;
747                        switch(orientation) {
748                            case ExifInterface.ORIENTATION_ROTATE_90:
749                                degree = 90;
750                                break;
751                            case ExifInterface.ORIENTATION_ROTATE_180:
752                                degree = 180;
753                                break;
754                            case ExifInterface.ORIENTATION_ROTATE_270:
755                                degree = 270;
756                                break;
757                            default:
758                                degree = 0;
759                                break;
760                        }
761                        values.put(Images.Media.ORIENTATION, degree);
762                    }
763                }
764            }
765
766            Uri result = null;
767            long rowId = entry.mRowId;
768            if (rowId == 0) {
769                // new file, insert it
770                result = mMediaProvider.insert(tableUri, values);
771                if (result != null) {
772                    rowId = ContentUris.parseId(result);
773                    entry.mRowId = rowId;
774                }
775            } else {
776                // updated file
777                result = ContentUris.withAppendedId(tableUri, rowId);
778                mMediaProvider.update(result, values, null, null);
779            }
780            if (mProcessGenres && mGenre != null) {
781                String genre = mGenre;
782                Uri uri = mGenreCache.get(genre);
783                if (uri == null) {
784                    Cursor cursor = null;
785                    try {
786                        // see if the genre already exists
787                        cursor = mMediaProvider.query(
788                                mGenresUri,
789                                GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?",
790                                        new String[] { genre }, null);
791                        if (cursor == null || cursor.getCount() == 0) {
792                            // genre does not exist, so create the genre in the genre table
793                            values.clear();
794                            values.put(MediaStore.Audio.Genres.NAME, genre);
795                            uri = mMediaProvider.insert(mGenresUri, values);
796                        } else {
797                            // genre already exists, so compute its Uri
798                            cursor.moveToNext();
799                            uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0));
800                        }
801                        if (uri != null) {
802                            uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY);
803                            mGenreCache.put(genre, uri);
804                        }
805                    } finally {
806                        // release the cursor if it exists
807                        if (cursor != null) {
808                            cursor.close();
809                        }
810                    }
811                }
812
813                if (uri != null) {
814                    // add entry to audio_genre_map
815                    values.clear();
816                    values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId));
817                    mMediaProvider.insert(uri, values);
818                }
819            }
820
821            if (notifications && !mDefaultNotificationSet) {
822                if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
823                        doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
824                    setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
825                    mDefaultNotificationSet = true;
826                }
827            } else if (ringtones && !mDefaultRingtoneSet) {
828                if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
829                        doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
830                    setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
831                    mDefaultRingtoneSet = true;
832                }
833            } else if (alarms && !mDefaultAlarmSet) {
834                if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
835                        doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
836                    setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
837                    mDefaultAlarmSet = true;
838                }
839            }
840
841            return result;
842        }
843
844        private boolean doesPathHaveFilename(String path, String filename) {
845            int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
846            int filenameLength = filename.length();
847            return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
848                    pathFilenameStart + filenameLength == path.length();
849        }
850
851        private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
852
853            String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
854                    settingName);
855
856            if (TextUtils.isEmpty(existingSettingValue)) {
857                // Set the setting to the given URI
858                Settings.System.putString(mContext.getContentResolver(), settingName,
859                        ContentUris.withAppendedId(uri, rowId).toString());
860            }
861        }
862
863        public void addNoMediaFolder(String path) {
864            ContentValues values = new ContentValues();
865            values.put(MediaStore.Images.ImageColumns.DATA, "");
866            String [] pathSpec = new String[] {path + '%'};
867            try {
868                mMediaProvider.update(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values,
869                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
870                mMediaProvider.update(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values,
871                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
872                mMediaProvider.update(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values,
873                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
874            } catch (RemoteException e) {
875                throw new RuntimeException();
876            }
877        }
878
879    }; // end of anonymous MediaScannerClient instance
880
881    private void prescan(String filePath) throws RemoteException {
882        Cursor c = null;
883        String where = null;
884        String[] selectionArgs = null;
885
886        if (mFileCache == null) {
887            mFileCache = new HashMap<String, FileCacheEntry>();
888        } else {
889            mFileCache.clear();
890        }
891        if (mPlayLists == null) {
892            mPlayLists = new ArrayList<FileCacheEntry>();
893        } else {
894            mPlayLists.clear();
895        }
896
897        // Build the list of files from the content provider
898        try {
899            // Read existing files from the audio table
900            if (filePath != null) {
901                where = MediaStore.Audio.Media.DATA + "=?";
902                selectionArgs = new String[] { filePath };
903            }
904            c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null);
905
906            if (c != null) {
907                try {
908                    while (c.moveToNext()) {
909                        long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX);
910                        String path = c.getString(PATH_AUDIO_COLUMN_INDEX);
911                        long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX);
912
913                        String key = path;
914                        if (mCaseInsensitivePaths) {
915                            key = path.toLowerCase();
916                        }
917                        mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path,
918                                lastModified));
919                    }
920                } finally {
921                    c.close();
922                    c = null;
923                }
924            }
925
926            // Read existing files from the video table
927            if (filePath != null) {
928                where = MediaStore.Video.Media.DATA + "=?";
929            } else {
930                where = null;
931            }
932            c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null);
933
934            if (c != null) {
935                try {
936                    while (c.moveToNext()) {
937                        long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX);
938                        String path = c.getString(PATH_VIDEO_COLUMN_INDEX);
939                        long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX);
940
941                        String key = path;
942                        if (mCaseInsensitivePaths) {
943                            key = path.toLowerCase();
944                        }
945                        mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path,
946                                lastModified));
947                    }
948                } finally {
949                    c.close();
950                    c = null;
951                }
952            }
953
954            // Read existing files from the images table
955            if (filePath != null) {
956                where = MediaStore.Images.Media.DATA + "=?";
957            } else {
958                where = null;
959            }
960            mOriginalCount = 0;
961            c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null);
962
963            if (c != null) {
964                try {
965                    mOriginalCount = c.getCount();
966                    while (c.moveToNext()) {
967                        long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX);
968                        String path = c.getString(PATH_IMAGES_COLUMN_INDEX);
969                       long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX);
970
971                        String key = path;
972                        if (mCaseInsensitivePaths) {
973                            key = path.toLowerCase();
974                        }
975                        mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path,
976                                lastModified));
977                    }
978                } finally {
979                    c.close();
980                    c = null;
981                }
982            }
983
984            if (mProcessPlaylists) {
985                // Read existing files from the playlists table
986                if (filePath != null) {
987                    where = MediaStore.Audio.Playlists.DATA + "=?";
988                } else {
989                    where = null;
990                }
991                c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null);
992
993                if (c != null) {
994                    try {
995                        while (c.moveToNext()) {
996                            String path = c.getString(PATH_IMAGES_COLUMN_INDEX);
997
998                            if (path != null && path.length() > 0) {
999                                long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX);
1000                                long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX);
1001
1002                                String key = path;
1003                                if (mCaseInsensitivePaths) {
1004                                    key = path.toLowerCase();
1005                                }
1006                                mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path,
1007                                        lastModified));
1008                            }
1009                        }
1010                    } finally {
1011                        c.close();
1012                        c = null;
1013                    }
1014                }
1015            }
1016        }
1017        finally {
1018            if (c != null) {
1019                c.close();
1020            }
1021        }
1022    }
1023
1024    private boolean inScanDirectory(String path, String[] directories) {
1025        for (int i = 0; i < directories.length; i++) {
1026            if (path.startsWith(directories[i])) {
1027                return true;
1028            }
1029        }
1030        return false;
1031    }
1032
1033    private void pruneDeadThumbnailFiles() {
1034        HashSet<String> existingFiles = new HashSet<String>();
1035        String directory = "/sdcard/DCIM/.thumbnails";
1036        String [] files = (new File(directory)).list();
1037        if (files == null)
1038            files = new String[0];
1039
1040        for (int i = 0; i < files.length; i++) {
1041            String fullPathString = directory + "/" + files[i];
1042            existingFiles.add(fullPathString);
1043        }
1044
1045        try {
1046            Cursor c = mMediaProvider.query(
1047                    mThumbsUri,
1048                    new String [] { "_data" },
1049                    null,
1050                    null,
1051                    null);
1052            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
1053            if (c != null && c.moveToFirst()) {
1054                do {
1055                    String fullPathString = c.getString(0);
1056                    existingFiles.remove(fullPathString);
1057                } while (c.moveToNext());
1058            }
1059
1060            for (String fileToDelete : existingFiles) {
1061                if (Config.LOGV)
1062                    Log.v(TAG, "fileToDelete is " + fileToDelete);
1063                try {
1064                    (new File(fileToDelete)).delete();
1065                } catch (SecurityException ex) {
1066                }
1067            }
1068
1069            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
1070            if (c != null) {
1071                c.close();
1072            }
1073        } catch (RemoteException e) {
1074            // We will soon be killed...
1075        }
1076    }
1077
1078    private void postscan(String[] directories) throws RemoteException {
1079        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1080
1081        while (iterator.hasNext()) {
1082            FileCacheEntry entry = iterator.next();
1083            String path = entry.mPath;
1084
1085            // remove database entries for files that no longer exist.
1086            boolean fileMissing = false;
1087
1088            if (!entry.mSeenInFileSystem) {
1089                if (inScanDirectory(path, directories)) {
1090                    // we didn't see this file in the scan directory.
1091                    fileMissing = true;
1092                } else {
1093                    // the file is outside of our scan directory,
1094                    // so we need to check for file existence here.
1095                    File testFile = new File(path);
1096                    if (!testFile.exists()) {
1097                        fileMissing = true;
1098                    }
1099                }
1100            }
1101
1102            if (fileMissing) {
1103                // do not delete missing playlists, since they may have been modified by the user.
1104                // the user can delete them in the media player instead.
1105                // instead, clear the path and lastModified fields in the row
1106                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1107                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1108
1109                if (MediaFile.isPlayListFileType(fileType)) {
1110                    ContentValues values = new ContentValues();
1111                    values.put(MediaStore.Audio.Playlists.DATA, "");
1112                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0);
1113                    mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null);
1114                } else {
1115                    mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null);
1116                    iterator.remove();
1117                }
1118            }
1119        }
1120
1121        // handle playlists last, after we know what media files are on the storage.
1122        if (mProcessPlaylists) {
1123            processPlayLists();
1124        }
1125
1126        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1127            pruneDeadThumbnailFiles();
1128
1129        // allow GC to clean up
1130        mGenreCache = null;
1131        mPlayLists = null;
1132        mFileCache = null;
1133        mMediaProvider = null;
1134    }
1135
1136    private void initialize(String volumeName) {
1137        mMediaProvider = mContext.getContentResolver().acquireProvider("media");
1138
1139        mAudioUri = Audio.Media.getContentUri(volumeName);
1140        mVideoUri = Video.Media.getContentUri(volumeName);
1141        mImagesUri = Images.Media.getContentUri(volumeName);
1142        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
1143
1144        if (!volumeName.equals("internal")) {
1145            // we only support playlists on external media
1146            mProcessPlaylists = true;
1147            mProcessGenres = true;
1148            mGenreCache = new HashMap<String, Uri>();
1149            mGenresUri = Genres.getContentUri(volumeName);
1150            mPlaylistsUri = Playlists.getContentUri(volumeName);
1151            // assuming external storage is FAT (case insensitive), except on the simulator.
1152            if ( Process.supportsProcesses()) {
1153                mCaseInsensitivePaths = true;
1154            }
1155        }
1156    }
1157
1158    public void scanDirectories(String[] directories, String volumeName) {
1159        try {
1160            long start = System.currentTimeMillis();
1161            initialize(volumeName);
1162            prescan(null);
1163            long prescan = System.currentTimeMillis();
1164
1165            for (int i = 0; i < directories.length; i++) {
1166                processDirectory(directories[i], MediaFile.sFileExtensions, mClient);
1167            }
1168            long scan = System.currentTimeMillis();
1169            postscan(directories);
1170            long end = System.currentTimeMillis();
1171
1172            if (Config.LOGD) {
1173                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1174                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1175                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1176                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1177            }
1178        } catch (SQLException e) {
1179            // this might happen if the SD card is removed while the media scanner is running
1180            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1181        } catch (UnsupportedOperationException e) {
1182            // this might happen if the SD card is removed while the media scanner is running
1183            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1184        } catch (RemoteException e) {
1185            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1186        }
1187    }
1188
1189    // this function is used to scan a single file
1190    public Uri scanSingleFile(String path, String volumeName, String mimeType) {
1191        try {
1192            initialize(volumeName);
1193            prescan(path);
1194
1195            File file = new File(path);
1196            // always scan the file, so we can return the content://media Uri for existing files
1197            return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true);
1198        } catch (RemoteException e) {
1199            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1200            return null;
1201        }
1202    }
1203
1204    // returns the number of matching file/directory names, starting from the right
1205    private int matchPaths(String path1, String path2) {
1206        int result = 0;
1207        int end1 = path1.length();
1208        int end2 = path2.length();
1209
1210        while (end1 > 0 && end2 > 0) {
1211            int slash1 = path1.lastIndexOf('/', end1 - 1);
1212            int slash2 = path2.lastIndexOf('/', end2 - 1);
1213            int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1214            int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1215            int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1216            int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1217            if (start1 < 0) start1 = 0; else start1++;
1218            if (start2 < 0) start2 = 0; else start2++;
1219            int length = end1 - start1;
1220            if (end2 - start2 != length) break;
1221            if (path1.regionMatches(true, start1, path2, start2, length)) {
1222                result++;
1223                end1 = start1 - 1;
1224                end2 = start2 - 1;
1225            } else break;
1226        }
1227
1228        return result;
1229    }
1230
1231    private boolean addPlayListEntry(String entry, String playListDirectory,
1232            Uri uri, ContentValues values, int index) {
1233
1234        // watch for trailing whitespace
1235        int entryLength = entry.length();
1236        while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--;
1237        // path should be longer than 3 characters.
1238        // avoid index out of bounds errors below by returning here.
1239        if (entryLength < 3) return false;
1240        if (entryLength < entry.length()) entry = entry.substring(0, entryLength);
1241
1242        // does entry appear to be an absolute path?
1243        // look for Unix or DOS absolute paths
1244        char ch1 = entry.charAt(0);
1245        boolean fullPath = (ch1 == '/' ||
1246                (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\'));
1247        // if we have a relative path, combine entry with playListDirectory
1248        if (!fullPath)
1249            entry = playListDirectory + entry;
1250
1251        //FIXME - should we look for "../" within the path?
1252
1253        // best matching MediaFile for the play list entry
1254        FileCacheEntry bestMatch = null;
1255
1256        // number of rightmost file/directory names for bestMatch
1257        int bestMatchLength = 0;
1258
1259        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1260        while (iterator.hasNext()) {
1261            FileCacheEntry cacheEntry = iterator.next();
1262            String path = cacheEntry.mPath;
1263
1264            if (path.equalsIgnoreCase(entry)) {
1265                bestMatch = cacheEntry;
1266                break;    // don't bother continuing search
1267            }
1268
1269            int matchLength = matchPaths(path, entry);
1270            if (matchLength > bestMatchLength) {
1271                bestMatch = cacheEntry;
1272                bestMatchLength = matchLength;
1273            }
1274        }
1275
1276        // if the match is not for an audio file, bail out
1277        if (bestMatch == null || ! mAudioUri.equals(bestMatch.mTableUri)) {
1278            return false;
1279        }
1280
1281        try {
1282        // OK, now we need to add this to the database
1283            values.clear();
1284            values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1285            values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId));
1286            mMediaProvider.insert(uri, values);
1287        } catch (RemoteException e) {
1288            Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e);
1289            return false;
1290        }
1291
1292        return true;
1293    }
1294
1295    private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1296        BufferedReader reader = null;
1297        try {
1298            File f = new File(path);
1299            if (f.exists()) {
1300                reader = new BufferedReader(
1301                        new InputStreamReader(new FileInputStream(f)), 8192);
1302                String line = reader.readLine();
1303                int index = 0;
1304                while (line != null) {
1305                    // ignore comment lines, which begin with '#'
1306                    if (line.length() > 0 && line.charAt(0) != '#') {
1307                        values.clear();
1308                        if (addPlayListEntry(line, playListDirectory, uri, values, index))
1309                            index++;
1310                    }
1311                    line = reader.readLine();
1312                }
1313            }
1314        } catch (IOException e) {
1315            Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1316        } finally {
1317            try {
1318                if (reader != null)
1319                    reader.close();
1320            } catch (IOException e) {
1321                Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1322            }
1323        }
1324    }
1325
1326    private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1327        BufferedReader reader = null;
1328        try {
1329            File f = new File(path);
1330            if (f.exists()) {
1331                reader = new BufferedReader(
1332                        new InputStreamReader(new FileInputStream(f)), 8192);
1333                String line = reader.readLine();
1334                int index = 0;
1335                while (line != null) {
1336                    // ignore comment lines, which begin with '#'
1337                    if (line.startsWith("File")) {
1338                        int equals = line.indexOf('=');
1339                        if (equals > 0) {
1340                            values.clear();
1341                            if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index))
1342                                index++;
1343                        }
1344                    }
1345                    line = reader.readLine();
1346                }
1347            }
1348        } catch (IOException e) {
1349            Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1350        } finally {
1351            try {
1352                if (reader != null)
1353                    reader.close();
1354            } catch (IOException e) {
1355                Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1356            }
1357        }
1358    }
1359
1360    class WplHandler implements ElementListener {
1361
1362        final ContentHandler handler;
1363        String playListDirectory;
1364        Uri uri;
1365        ContentValues values = new ContentValues();
1366        int index = 0;
1367
1368        public WplHandler(String playListDirectory, Uri uri) {
1369            this.playListDirectory = playListDirectory;
1370            this.uri = uri;
1371
1372            RootElement root = new RootElement("smil");
1373            Element body = root.getChild("body");
1374            Element seq = body.getChild("seq");
1375            Element media = seq.getChild("media");
1376            media.setElementListener(this);
1377
1378            this.handler = root.getContentHandler();
1379        }
1380
1381        public void start(Attributes attributes) {
1382            String path = attributes.getValue("", "src");
1383            if (path != null) {
1384                values.clear();
1385                if (addPlayListEntry(path, playListDirectory, uri, values, index)) {
1386                    index++;
1387                }
1388            }
1389        }
1390
1391       public void end() {
1392       }
1393
1394        ContentHandler getContentHandler() {
1395            return handler;
1396        }
1397    }
1398
1399    private void processWplPlayList(String path, String playListDirectory, Uri uri) {
1400        FileInputStream fis = null;
1401        try {
1402            File f = new File(path);
1403            if (f.exists()) {
1404                fis = new FileInputStream(f);
1405
1406                Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler());
1407            }
1408        } catch (SAXException e) {
1409            e.printStackTrace();
1410        } catch (IOException e) {
1411            e.printStackTrace();
1412        } finally {
1413            try {
1414                if (fis != null)
1415                    fis.close();
1416            } catch (IOException e) {
1417                Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1418            }
1419        }
1420    }
1421
1422    private void processPlayLists() throws RemoteException {
1423        Iterator<FileCacheEntry> iterator = mPlayLists.iterator();
1424        while (iterator.hasNext()) {
1425            FileCacheEntry entry = iterator.next();
1426            String path = entry.mPath;
1427
1428            // only process playlist files if they are new or have been modified since the last scan
1429            if (entry.mLastModifiedChanged) {
1430                ContentValues values = new ContentValues();
1431                int lastSlash = path.lastIndexOf('/');
1432                if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1433                Uri uri, membersUri;
1434                long rowId = entry.mRowId;
1435                if (rowId == 0) {
1436                    // Create a new playlist
1437
1438                    int lastDot = path.lastIndexOf('.');
1439                    String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot));
1440                    values.put(MediaStore.Audio.Playlists.NAME, name);
1441                    values.put(MediaStore.Audio.Playlists.DATA, path);
1442                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1443                    uri = mMediaProvider.insert(mPlaylistsUri, values);
1444                    rowId = ContentUris.parseId(uri);
1445                    membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1446                } else {
1447                    uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1448
1449                    // update lastModified value of existing playlist
1450                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1451                    mMediaProvider.update(uri, values, null, null);
1452
1453                    // delete members of existing playlist
1454                    membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1455                    mMediaProvider.delete(membersUri, null, null);
1456                }
1457
1458                String playListDirectory = path.substring(0, lastSlash + 1);
1459                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1460                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1461
1462                if (fileType == MediaFile.FILE_TYPE_M3U)
1463                    processM3uPlayList(path, playListDirectory, membersUri, values);
1464                else if (fileType == MediaFile.FILE_TYPE_PLS)
1465                    processPlsPlayList(path, playListDirectory, membersUri, values);
1466                else if (fileType == MediaFile.FILE_TYPE_WPL)
1467                    processWplPlayList(path, playListDirectory, membersUri);
1468
1469                Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null,
1470                        null, null);
1471                try {
1472                    if (cursor == null || cursor.getCount() == 0) {
1473                        Log.d(TAG, "playlist is empty - deleting");
1474                        mMediaProvider.delete(uri, null, null);
1475                    }
1476                } finally {
1477                    if (cursor != null) cursor.close();
1478                }
1479            }
1480        }
1481    }
1482
1483    private native void processDirectory(String path, String extensions, MediaScannerClient client);
1484    private native void processFile(String path, String mimeType, MediaScannerClient client);
1485    public native void setLocale(String locale);
1486
1487    public native byte[] extractAlbumArt(FileDescriptor fd);
1488
1489    private static native final void native_init();
1490    private native final void native_setup();
1491    private native final void native_finalize();
1492    @Override
1493    protected void finalize() {
1494        mContext.getContentResolver().releaseProvider(mMediaProvider);
1495        native_finalize();
1496    }
1497}
1498