MediaScanner.java revision 35b002a9c873fb40116179c8626b9841b0abae74
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 : MediaStore.UNKNOWN_STRING));
641                map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaStore.UNKNOWN_STRING));
642                map.put(Video.Media.DURATION, mDuration);
643                map.put(Video.Media.DATE_TAKEN, mLastModified * 1000);
644                // FIXME - add RESOLUTION
645            } else if (MediaFile.isImageFileType(mFileType)) {
646                // FIXME - add DESCRIPTION
647                // DATE_TAKEN will be overridden later if this is a JPEG image whose EXIF data
648                // contains date time information.
649                map.put(Images.Media.DATE_TAKEN, mLastModified * 1000);
650            } else if (MediaFile.isAudioFileType(mFileType)) {
651                map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaStore.UNKNOWN_STRING));
652                map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaStore.UNKNOWN_STRING));
653                map.put(Audio.Media.COMPOSER, mComposer);
654                if (mYear != 0) {
655                    map.put(Audio.Media.YEAR, mYear);
656                }
657                map.put(Audio.Media.TRACK, mTrack);
658                map.put(Audio.Media.DURATION, mDuration);
659            }
660            return map;
661        }
662
663        private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,
664                boolean alarms, boolean music, boolean podcasts)
665                throws RemoteException {
666            // update database
667            Uri tableUri;
668            boolean isAudio = MediaFile.isAudioFileType(mFileType);
669            boolean isVideo = MediaFile.isVideoFileType(mFileType);
670            boolean isImage = MediaFile.isImageFileType(mFileType);
671            if (isVideo) {
672                tableUri = mVideoUri;
673            } else if (isImage) {
674                tableUri = mImagesUri;
675            } else if (isAudio) {
676                tableUri = mAudioUri;
677            } else {
678                // don't add file to database if not audio, video or image
679                return null;
680            }
681            entry.mTableUri = tableUri;
682
683             // use album artist if artist is missing
684            if (mArtist == null || mArtist.length() == 0) {
685                mArtist = mAlbumArtist;
686            }
687
688            ContentValues values = toValues();
689            String title = values.getAsString(MediaStore.MediaColumns.TITLE);
690            if (title == null || TextUtils.isEmpty(title.trim())) {
691                title = values.getAsString(MediaStore.MediaColumns.DATA);
692                // extract file name after last slash
693                int lastSlash = title.lastIndexOf('/');
694                if (lastSlash >= 0) {
695                    lastSlash++;
696                    if (lastSlash < title.length()) {
697                        title = title.substring(lastSlash);
698                    }
699                }
700                // truncate the file extension (if any)
701                int lastDot = title.lastIndexOf('.');
702                if (lastDot > 0) {
703                    title = title.substring(0, lastDot);
704                }
705                values.put(MediaStore.MediaColumns.TITLE, title);
706            }
707            String album = values.getAsString(Audio.Media.ALBUM);
708            if (MediaStore.UNKNOWN_STRING.equals(album)) {
709                album = values.getAsString(MediaStore.MediaColumns.DATA);
710                // extract last path segment before file name
711                int lastSlash = album.lastIndexOf('/');
712                if (lastSlash >= 0) {
713                    int previousSlash = 0;
714                    while (true) {
715                        int idx = album.indexOf('/', previousSlash + 1);
716                        if (idx < 0 || idx >= lastSlash) {
717                            break;
718                        }
719                        previousSlash = idx;
720                    }
721                    if (previousSlash != 0) {
722                        album = album.substring(previousSlash + 1, lastSlash);
723                        values.put(Audio.Media.ALBUM, album);
724                    }
725                }
726            }
727            if (isAudio) {
728                values.put(Audio.Media.IS_RINGTONE, ringtones);
729                values.put(Audio.Media.IS_NOTIFICATION, notifications);
730                values.put(Audio.Media.IS_ALARM, alarms);
731                values.put(Audio.Media.IS_MUSIC, music);
732                values.put(Audio.Media.IS_PODCAST, podcasts);
733            } else if (mFileType == MediaFile.FILE_TYPE_JPEG) {
734                ExifInterface exif = null;
735                try {
736                    exif = new ExifInterface(entry.mPath);
737                } catch (IOException ex) {
738                    // exif is null
739                }
740                if (exif != null) {
741                    float[] latlng = new float[2];
742                    if (exif.getLatLong(latlng)) {
743                        values.put(Images.Media.LATITUDE, latlng[0]);
744                        values.put(Images.Media.LONGITUDE, latlng[1]);
745                    }
746
747                    long time = exif.getDateTime();
748                    if (time != -1) {
749                        values.put(Images.Media.DATE_TAKEN, time);
750                    }
751
752                    int orientation = exif.getAttributeInt(
753                        ExifInterface.TAG_ORIENTATION, -1);
754                    if (orientation != -1) {
755                        // We only recognize a subset of orientation tag values.
756                        int degree;
757                        switch(orientation) {
758                            case ExifInterface.ORIENTATION_ROTATE_90:
759                                degree = 90;
760                                break;
761                            case ExifInterface.ORIENTATION_ROTATE_180:
762                                degree = 180;
763                                break;
764                            case ExifInterface.ORIENTATION_ROTATE_270:
765                                degree = 270;
766                                break;
767                            default:
768                                degree = 0;
769                                break;
770                        }
771                        values.put(Images.Media.ORIENTATION, degree);
772                    }
773                }
774            }
775
776            Uri result = null;
777            long rowId = entry.mRowId;
778            if (rowId == 0) {
779                // new file, insert it
780                result = mMediaProvider.insert(tableUri, values);
781                if (result != null) {
782                    rowId = ContentUris.parseId(result);
783                    entry.mRowId = rowId;
784                }
785            } else {
786                // updated file
787                result = ContentUris.withAppendedId(tableUri, rowId);
788                mMediaProvider.update(result, values, null, null);
789            }
790            if (mProcessGenres && mGenre != null) {
791                String genre = mGenre;
792                Uri uri = mGenreCache.get(genre);
793                if (uri == null) {
794                    Cursor cursor = null;
795                    try {
796                        // see if the genre already exists
797                        cursor = mMediaProvider.query(
798                                mGenresUri,
799                                GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?",
800                                        new String[] { genre }, null);
801                        if (cursor == null || cursor.getCount() == 0) {
802                            // genre does not exist, so create the genre in the genre table
803                            values.clear();
804                            values.put(MediaStore.Audio.Genres.NAME, genre);
805                            uri = mMediaProvider.insert(mGenresUri, values);
806                        } else {
807                            // genre already exists, so compute its Uri
808                            cursor.moveToNext();
809                            uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0));
810                        }
811                        if (uri != null) {
812                            uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY);
813                            mGenreCache.put(genre, uri);
814                        }
815                    } finally {
816                        // release the cursor if it exists
817                        if (cursor != null) {
818                            cursor.close();
819                        }
820                    }
821                }
822
823                if (uri != null) {
824                    // add entry to audio_genre_map
825                    values.clear();
826                    values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId));
827                    mMediaProvider.insert(uri, values);
828                }
829            }
830
831            if (notifications && !mDefaultNotificationSet) {
832                if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
833                        doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
834                    setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
835                    mDefaultNotificationSet = true;
836                }
837            } else if (ringtones && !mDefaultRingtoneSet) {
838                if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
839                        doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
840                    setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
841                    mDefaultRingtoneSet = true;
842                }
843            } else if (alarms && !mDefaultAlarmSet) {
844                if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
845                        doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
846                    setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
847                    mDefaultAlarmSet = true;
848                }
849            }
850
851            return result;
852        }
853
854        private boolean doesPathHaveFilename(String path, String filename) {
855            int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
856            int filenameLength = filename.length();
857            return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
858                    pathFilenameStart + filenameLength == path.length();
859        }
860
861        private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
862
863            String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
864                    settingName);
865
866            if (TextUtils.isEmpty(existingSettingValue)) {
867                // Set the setting to the given URI
868                Settings.System.putString(mContext.getContentResolver(), settingName,
869                        ContentUris.withAppendedId(uri, rowId).toString());
870            }
871        }
872
873        public void addNoMediaFolder(String path) {
874            ContentValues values = new ContentValues();
875            values.put(MediaStore.Images.ImageColumns.DATA, "");
876            String [] pathSpec = new String[] {path + '%'};
877            try {
878                mMediaProvider.update(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values,
879                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
880                mMediaProvider.update(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values,
881                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
882                mMediaProvider.update(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values,
883                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
884            } catch (RemoteException e) {
885                throw new RuntimeException();
886            }
887        }
888
889    }; // end of anonymous MediaScannerClient instance
890
891    private void prescan(String filePath) throws RemoteException {
892        Cursor c = null;
893        String where = null;
894        String[] selectionArgs = null;
895
896        if (mFileCache == null) {
897            mFileCache = new HashMap<String, FileCacheEntry>();
898        } else {
899            mFileCache.clear();
900        }
901        if (mPlayLists == null) {
902            mPlayLists = new ArrayList<FileCacheEntry>();
903        } else {
904            mPlayLists.clear();
905        }
906
907        // Build the list of files from the content provider
908        try {
909            // Read existing files from the audio table
910            if (filePath != null) {
911                where = MediaStore.Audio.Media.DATA + "=?";
912                selectionArgs = new String[] { filePath };
913            }
914            c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null);
915
916            if (c != null) {
917                try {
918                    while (c.moveToNext()) {
919                        long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX);
920                        String path = c.getString(PATH_AUDIO_COLUMN_INDEX);
921                        long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX);
922
923                        // Only consider entries with absolute path names.
924                        // This allows storing URIs in the database without the
925                        // media scanner removing them.
926                        if (path.startsWith("/")) {
927                            String key = path;
928                            if (mCaseInsensitivePaths) {
929                                key = path.toLowerCase();
930                            }
931                            mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path,
932                                    lastModified));
933                        }
934                    }
935                } finally {
936                    c.close();
937                    c = null;
938                }
939            }
940
941            // Read existing files from the video table
942            if (filePath != null) {
943                where = MediaStore.Video.Media.DATA + "=?";
944            } else {
945                where = null;
946            }
947            c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null);
948
949            if (c != null) {
950                try {
951                    while (c.moveToNext()) {
952                        long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX);
953                        String path = c.getString(PATH_VIDEO_COLUMN_INDEX);
954                        long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX);
955
956                        // Only consider entries with absolute path names.
957                        // This allows storing URIs in the database without the
958                        // media scanner removing them.
959                        if (path.startsWith("/")) {
960                            String key = path;
961                            if (mCaseInsensitivePaths) {
962                                key = path.toLowerCase();
963                            }
964                            mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path,
965                                    lastModified));
966                        }
967                    }
968                } finally {
969                    c.close();
970                    c = null;
971                }
972            }
973
974            // Read existing files from the images table
975            if (filePath != null) {
976                where = MediaStore.Images.Media.DATA + "=?";
977            } else {
978                where = null;
979            }
980            mOriginalCount = 0;
981            c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null);
982
983            if (c != null) {
984                try {
985                    mOriginalCount = c.getCount();
986                    while (c.moveToNext()) {
987                        long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX);
988                        String path = c.getString(PATH_IMAGES_COLUMN_INDEX);
989                       long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX);
990
991                       // Only consider entries with absolute path names.
992                       // This allows storing URIs in the database without the
993                       // media scanner removing them.
994                       if (path.startsWith("/")) {
995                           String key = path;
996                           if (mCaseInsensitivePaths) {
997                               key = path.toLowerCase();
998                           }
999                           mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path,
1000                                   lastModified));
1001                       }
1002                    }
1003                } finally {
1004                    c.close();
1005                    c = null;
1006                }
1007            }
1008
1009            if (mProcessPlaylists) {
1010                // Read existing files from the playlists table
1011                if (filePath != null) {
1012                    where = MediaStore.Audio.Playlists.DATA + "=?";
1013                } else {
1014                    where = null;
1015                }
1016                c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null);
1017
1018                if (c != null) {
1019                    try {
1020                        while (c.moveToNext()) {
1021                            String path = c.getString(PATH_PLAYLISTS_COLUMN_INDEX);
1022
1023                            if (path != null && path.length() > 0) {
1024                                long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX);
1025                                long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX);
1026
1027                                String key = path;
1028                                if (mCaseInsensitivePaths) {
1029                                    key = path.toLowerCase();
1030                                }
1031                                mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path,
1032                                        lastModified));
1033                            }
1034                        }
1035                    } finally {
1036                        c.close();
1037                        c = null;
1038                    }
1039                }
1040            }
1041        }
1042        finally {
1043            if (c != null) {
1044                c.close();
1045            }
1046        }
1047    }
1048
1049    private boolean inScanDirectory(String path, String[] directories) {
1050        for (int i = 0; i < directories.length; i++) {
1051            if (path.startsWith(directories[i])) {
1052                return true;
1053            }
1054        }
1055        return false;
1056    }
1057
1058    private void pruneDeadThumbnailFiles() {
1059        HashSet<String> existingFiles = new HashSet<String>();
1060        String directory = "/sdcard/DCIM/.thumbnails";
1061        String [] files = (new File(directory)).list();
1062        if (files == null)
1063            files = new String[0];
1064
1065        for (int i = 0; i < files.length; i++) {
1066            String fullPathString = directory + "/" + files[i];
1067            existingFiles.add(fullPathString);
1068        }
1069
1070        try {
1071            Cursor c = mMediaProvider.query(
1072                    mThumbsUri,
1073                    new String [] { "_data" },
1074                    null,
1075                    null,
1076                    null);
1077            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
1078            if (c != null && c.moveToFirst()) {
1079                do {
1080                    String fullPathString = c.getString(0);
1081                    existingFiles.remove(fullPathString);
1082                } while (c.moveToNext());
1083            }
1084
1085            for (String fileToDelete : existingFiles) {
1086                if (Config.LOGV)
1087                    Log.v(TAG, "fileToDelete is " + fileToDelete);
1088                try {
1089                    (new File(fileToDelete)).delete();
1090                } catch (SecurityException ex) {
1091                }
1092            }
1093
1094            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
1095            if (c != null) {
1096                c.close();
1097            }
1098        } catch (RemoteException e) {
1099            // We will soon be killed...
1100        }
1101    }
1102
1103    private void postscan(String[] directories) throws RemoteException {
1104        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1105
1106        while (iterator.hasNext()) {
1107            FileCacheEntry entry = iterator.next();
1108            String path = entry.mPath;
1109
1110            // remove database entries for files that no longer exist.
1111            boolean fileMissing = false;
1112
1113            if (!entry.mSeenInFileSystem) {
1114                if (inScanDirectory(path, directories)) {
1115                    // we didn't see this file in the scan directory.
1116                    fileMissing = true;
1117                } else {
1118                    // the file is outside of our scan directory,
1119                    // so we need to check for file existence here.
1120                    File testFile = new File(path);
1121                    if (!testFile.exists()) {
1122                        fileMissing = true;
1123                    }
1124                }
1125            }
1126
1127            if (fileMissing) {
1128                // do not delete missing playlists, since they may have been modified by the user.
1129                // the user can delete them in the media player instead.
1130                // instead, clear the path and lastModified fields in the row
1131                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1132                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1133
1134                if (MediaFile.isPlayListFileType(fileType)) {
1135                    ContentValues values = new ContentValues();
1136                    values.put(MediaStore.Audio.Playlists.DATA, "");
1137                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0);
1138                    mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null);
1139                } else {
1140                    mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null);
1141                    iterator.remove();
1142                }
1143            }
1144        }
1145
1146        // handle playlists last, after we know what media files are on the storage.
1147        if (mProcessPlaylists) {
1148            processPlayLists();
1149        }
1150
1151        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1152            pruneDeadThumbnailFiles();
1153
1154        // allow GC to clean up
1155        mGenreCache = null;
1156        mPlayLists = null;
1157        mFileCache = null;
1158        mMediaProvider = null;
1159    }
1160
1161    private void initialize(String volumeName) {
1162        mMediaProvider = mContext.getContentResolver().acquireProvider("media");
1163
1164        mAudioUri = Audio.Media.getContentUri(volumeName);
1165        mVideoUri = Video.Media.getContentUri(volumeName);
1166        mImagesUri = Images.Media.getContentUri(volumeName);
1167        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
1168
1169        if (!volumeName.equals("internal")) {
1170            // we only support playlists on external media
1171            mProcessPlaylists = true;
1172            mProcessGenres = true;
1173            mGenreCache = new HashMap<String, Uri>();
1174            mGenresUri = Genres.getContentUri(volumeName);
1175            mPlaylistsUri = Playlists.getContentUri(volumeName);
1176            // assuming external storage is FAT (case insensitive), except on the simulator.
1177            if ( Process.supportsProcesses()) {
1178                mCaseInsensitivePaths = true;
1179            }
1180        }
1181    }
1182
1183    public void scanDirectories(String[] directories, String volumeName) {
1184        try {
1185            long start = System.currentTimeMillis();
1186            initialize(volumeName);
1187            prescan(null);
1188            long prescan = System.currentTimeMillis();
1189
1190            for (int i = 0; i < directories.length; i++) {
1191                processDirectory(directories[i], MediaFile.sFileExtensions, mClient);
1192            }
1193            long scan = System.currentTimeMillis();
1194            postscan(directories);
1195            long end = System.currentTimeMillis();
1196
1197            if (Config.LOGD) {
1198                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1199                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1200                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1201                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1202            }
1203        } catch (SQLException e) {
1204            // this might happen if the SD card is removed while the media scanner is running
1205            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1206        } catch (UnsupportedOperationException e) {
1207            // this might happen if the SD card is removed while the media scanner is running
1208            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1209        } catch (RemoteException e) {
1210            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1211        }
1212    }
1213
1214    // this function is used to scan a single file
1215    public Uri scanSingleFile(String path, String volumeName, String mimeType) {
1216        try {
1217            initialize(volumeName);
1218            prescan(path);
1219
1220            File file = new File(path);
1221            // always scan the file, so we can return the content://media Uri for existing files
1222            return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true);
1223        } catch (RemoteException e) {
1224            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1225            return null;
1226        }
1227    }
1228
1229    // returns the number of matching file/directory names, starting from the right
1230    private int matchPaths(String path1, String path2) {
1231        int result = 0;
1232        int end1 = path1.length();
1233        int end2 = path2.length();
1234
1235        while (end1 > 0 && end2 > 0) {
1236            int slash1 = path1.lastIndexOf('/', end1 - 1);
1237            int slash2 = path2.lastIndexOf('/', end2 - 1);
1238            int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1239            int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1240            int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1241            int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1242            if (start1 < 0) start1 = 0; else start1++;
1243            if (start2 < 0) start2 = 0; else start2++;
1244            int length = end1 - start1;
1245            if (end2 - start2 != length) break;
1246            if (path1.regionMatches(true, start1, path2, start2, length)) {
1247                result++;
1248                end1 = start1 - 1;
1249                end2 = start2 - 1;
1250            } else break;
1251        }
1252
1253        return result;
1254    }
1255
1256    private boolean addPlayListEntry(String entry, String playListDirectory,
1257            Uri uri, ContentValues values, int index) {
1258
1259        // watch for trailing whitespace
1260        int entryLength = entry.length();
1261        while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--;
1262        // path should be longer than 3 characters.
1263        // avoid index out of bounds errors below by returning here.
1264        if (entryLength < 3) return false;
1265        if (entryLength < entry.length()) entry = entry.substring(0, entryLength);
1266
1267        // does entry appear to be an absolute path?
1268        // look for Unix or DOS absolute paths
1269        char ch1 = entry.charAt(0);
1270        boolean fullPath = (ch1 == '/' ||
1271                (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\'));
1272        // if we have a relative path, combine entry with playListDirectory
1273        if (!fullPath)
1274            entry = playListDirectory + entry;
1275
1276        //FIXME - should we look for "../" within the path?
1277
1278        // best matching MediaFile for the play list entry
1279        FileCacheEntry bestMatch = null;
1280
1281        // number of rightmost file/directory names for bestMatch
1282        int bestMatchLength = 0;
1283
1284        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1285        while (iterator.hasNext()) {
1286            FileCacheEntry cacheEntry = iterator.next();
1287            String path = cacheEntry.mPath;
1288
1289            if (path.equalsIgnoreCase(entry)) {
1290                bestMatch = cacheEntry;
1291                break;    // don't bother continuing search
1292            }
1293
1294            int matchLength = matchPaths(path, entry);
1295            if (matchLength > bestMatchLength) {
1296                bestMatch = cacheEntry;
1297                bestMatchLength = matchLength;
1298            }
1299        }
1300
1301        // if the match is not for an audio file, bail out
1302        if (bestMatch == null || ! mAudioUri.equals(bestMatch.mTableUri)) {
1303            return false;
1304        }
1305
1306        try {
1307        // OK, now we need to add this to the database
1308            values.clear();
1309            values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1310            values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId));
1311            mMediaProvider.insert(uri, values);
1312        } catch (RemoteException e) {
1313            Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e);
1314            return false;
1315        }
1316
1317        return true;
1318    }
1319
1320    private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1321        BufferedReader reader = null;
1322        try {
1323            File f = new File(path);
1324            if (f.exists()) {
1325                reader = new BufferedReader(
1326                        new InputStreamReader(new FileInputStream(f)), 8192);
1327                String line = reader.readLine();
1328                int index = 0;
1329                while (line != null) {
1330                    // ignore comment lines, which begin with '#'
1331                    if (line.length() > 0 && line.charAt(0) != '#') {
1332                        values.clear();
1333                        if (addPlayListEntry(line, playListDirectory, uri, values, index))
1334                            index++;
1335                    }
1336                    line = reader.readLine();
1337                }
1338            }
1339        } catch (IOException e) {
1340            Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1341        } finally {
1342            try {
1343                if (reader != null)
1344                    reader.close();
1345            } catch (IOException e) {
1346                Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1347            }
1348        }
1349    }
1350
1351    private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1352        BufferedReader reader = null;
1353        try {
1354            File f = new File(path);
1355            if (f.exists()) {
1356                reader = new BufferedReader(
1357                        new InputStreamReader(new FileInputStream(f)), 8192);
1358                String line = reader.readLine();
1359                int index = 0;
1360                while (line != null) {
1361                    // ignore comment lines, which begin with '#'
1362                    if (line.startsWith("File")) {
1363                        int equals = line.indexOf('=');
1364                        if (equals > 0) {
1365                            values.clear();
1366                            if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index))
1367                                index++;
1368                        }
1369                    }
1370                    line = reader.readLine();
1371                }
1372            }
1373        } catch (IOException e) {
1374            Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1375        } finally {
1376            try {
1377                if (reader != null)
1378                    reader.close();
1379            } catch (IOException e) {
1380                Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1381            }
1382        }
1383    }
1384
1385    class WplHandler implements ElementListener {
1386
1387        final ContentHandler handler;
1388        String playListDirectory;
1389        Uri uri;
1390        ContentValues values = new ContentValues();
1391        int index = 0;
1392
1393        public WplHandler(String playListDirectory, Uri uri) {
1394            this.playListDirectory = playListDirectory;
1395            this.uri = uri;
1396
1397            RootElement root = new RootElement("smil");
1398            Element body = root.getChild("body");
1399            Element seq = body.getChild("seq");
1400            Element media = seq.getChild("media");
1401            media.setElementListener(this);
1402
1403            this.handler = root.getContentHandler();
1404        }
1405
1406        public void start(Attributes attributes) {
1407            String path = attributes.getValue("", "src");
1408            if (path != null) {
1409                values.clear();
1410                if (addPlayListEntry(path, playListDirectory, uri, values, index)) {
1411                    index++;
1412                }
1413            }
1414        }
1415
1416       public void end() {
1417       }
1418
1419        ContentHandler getContentHandler() {
1420            return handler;
1421        }
1422    }
1423
1424    private void processWplPlayList(String path, String playListDirectory, Uri uri) {
1425        FileInputStream fis = null;
1426        try {
1427            File f = new File(path);
1428            if (f.exists()) {
1429                fis = new FileInputStream(f);
1430
1431                Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler());
1432            }
1433        } catch (SAXException e) {
1434            e.printStackTrace();
1435        } catch (IOException e) {
1436            e.printStackTrace();
1437        } finally {
1438            try {
1439                if (fis != null)
1440                    fis.close();
1441            } catch (IOException e) {
1442                Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1443            }
1444        }
1445    }
1446
1447    private void processPlayLists() throws RemoteException {
1448        Iterator<FileCacheEntry> iterator = mPlayLists.iterator();
1449        while (iterator.hasNext()) {
1450            FileCacheEntry entry = iterator.next();
1451            String path = entry.mPath;
1452
1453            // only process playlist files if they are new or have been modified since the last scan
1454            if (entry.mLastModifiedChanged) {
1455                ContentValues values = new ContentValues();
1456                int lastSlash = path.lastIndexOf('/');
1457                if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1458                Uri uri, membersUri;
1459                long rowId = entry.mRowId;
1460                if (rowId == 0) {
1461                    // Create a new playlist
1462
1463                    int lastDot = path.lastIndexOf('.');
1464                    String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot));
1465                    values.put(MediaStore.Audio.Playlists.NAME, name);
1466                    values.put(MediaStore.Audio.Playlists.DATA, path);
1467                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1468                    uri = mMediaProvider.insert(mPlaylistsUri, values);
1469                    rowId = ContentUris.parseId(uri);
1470                    membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1471                } else {
1472                    uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1473
1474                    // update lastModified value of existing playlist
1475                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1476                    mMediaProvider.update(uri, values, null, null);
1477
1478                    // delete members of existing playlist
1479                    membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1480                    mMediaProvider.delete(membersUri, null, null);
1481                }
1482
1483                String playListDirectory = path.substring(0, lastSlash + 1);
1484                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1485                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1486
1487                if (fileType == MediaFile.FILE_TYPE_M3U)
1488                    processM3uPlayList(path, playListDirectory, membersUri, values);
1489                else if (fileType == MediaFile.FILE_TYPE_PLS)
1490                    processPlsPlayList(path, playListDirectory, membersUri, values);
1491                else if (fileType == MediaFile.FILE_TYPE_WPL)
1492                    processWplPlayList(path, playListDirectory, membersUri);
1493
1494                Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null,
1495                        null, null);
1496                try {
1497                    if (cursor == null || cursor.getCount() == 0) {
1498                        Log.d(TAG, "playlist is empty - deleting");
1499                        mMediaProvider.delete(uri, null, null);
1500                    }
1501                } finally {
1502                    if (cursor != null) cursor.close();
1503                }
1504            }
1505        }
1506    }
1507
1508    private native void processDirectory(String path, String extensions, MediaScannerClient client);
1509    private native void processFile(String path, String mimeType, MediaScannerClient client);
1510    public native void setLocale(String locale);
1511
1512    public native byte[] extractAlbumArt(FileDescriptor fd);
1513
1514    private static native final void native_init();
1515    private native final void native_setup();
1516    private native final void native_finalize();
1517    @Override
1518    protected void finalize() {
1519        mContext.getContentResolver().releaseProvider(mMediaProvider);
1520        native_finalize();
1521    }
1522}
1523