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