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