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