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