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