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