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