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