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