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