MediaScanner.java revision cbaea35759791f083735f7a5c43e0dc0ed228ff1
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    private int mMtpObjectHandle;
309
310    // used when scanning the image database so we know whether we have to prune
311    // old thumbnail files
312    private int mOriginalCount;
313    /** Whether the scanner has set a default sound for the ringer ringtone. */
314    private boolean mDefaultRingtoneSet;
315    /** Whether the scanner has set a default sound for the notification ringtone. */
316    private boolean mDefaultNotificationSet;
317    /** Whether the scanner has set a default sound for the alarm ringtone. */
318    private boolean mDefaultAlarmSet;
319    /** The filename for the default sound for the ringer ringtone. */
320    private String mDefaultRingtoneFilename;
321    /** The filename for the default sound for the notification ringtone. */
322    private String mDefaultNotificationFilename;
323    /** The filename for the default sound for the alarm ringtone. */
324    private String mDefaultAlarmAlertFilename;
325    /**
326     * The prefix for system properties that define the default sound for
327     * ringtones. Concatenate the name of the setting from Settings
328     * to get the full system property.
329     */
330    private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config.";
331
332    // set to true if file path comparisons should be case insensitive.
333    // this should be set when scanning files on a case insensitive file system.
334    private boolean mCaseInsensitivePaths;
335
336    private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options();
337
338    private static class FileCacheEntry {
339        Uri mTableUri;
340        long mRowId;
341        String mPath;
342        long mLastModified;
343        boolean mSeenInFileSystem;
344        boolean mLastModifiedChanged;
345
346        FileCacheEntry(Uri tableUri, long rowId, String path, long lastModified) {
347            mTableUri = tableUri;
348            mRowId = rowId;
349            mPath = path;
350            mLastModified = lastModified;
351            mSeenInFileSystem = false;
352            mLastModifiedChanged = false;
353        }
354
355        @Override
356        public String toString() {
357            return mPath;
358        }
359    }
360
361    // hashes file path to FileCacheEntry.
362    // path should be lower case if mCaseInsensitivePaths is true
363    private HashMap<String, FileCacheEntry> mFileCache;
364
365    private ArrayList<FileCacheEntry> mPlayLists;
366    private HashMap<String, Uri> mGenreCache;
367
368
369    public MediaScanner(Context c) {
370        native_setup();
371        mContext = c;
372        mBitmapOptions.inSampleSize = 1;
373        mBitmapOptions.inJustDecodeBounds = true;
374
375        setDefaultRingtoneFileNames();
376    }
377
378    private void setDefaultRingtoneFileNames() {
379        mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
380                + Settings.System.RINGTONE);
381        mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
382                + Settings.System.NOTIFICATION_SOUND);
383        mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
384                + Settings.System.ALARM_ALERT);
385    }
386
387    private MyMediaScannerClient mClient = new MyMediaScannerClient();
388
389    private class MyMediaScannerClient implements MediaScannerClient {
390
391        private String mArtist;
392        private String mAlbumArtist;    // use this if mArtist is missing
393        private String mAlbum;
394        private String mTitle;
395        private String mComposer;
396        private String mGenre;
397        private String mMimeType;
398        private int mFileType;
399        private int mTrack;
400        private int mYear;
401        private int mDuration;
402        private String mPath;
403        private long mLastModified;
404        private long mFileSize;
405        private String mWriter;
406
407        public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) {
408
409            // special case certain file names
410            // I use regionMatches() instead of substring() below
411            // to avoid memory allocation
412            int lastSlash = path.lastIndexOf('/');
413            if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
414                // ignore those ._* files created by MacOS
415                if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
416                    return null;
417                }
418
419                // ignore album art files created by Windows Media Player:
420                // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg and AlbumArt_{...}_Small.jpg
421                if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
422                    if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
423                            path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
424                        return null;
425                    }
426                    int length = path.length() - lastSlash - 1;
427                    if ((length == 17 && path.regionMatches(true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
428                            (length == 10 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
429                        return null;
430                    }
431                }
432            }
433
434            mMimeType = null;
435            // try mimeType first, if it is specified
436            if (mimeType != null) {
437                mFileType = MediaFile.getFileTypeForMimeType(mimeType);
438                if (mFileType != 0) {
439                    mMimeType = mimeType;
440                }
441            }
442            mFileSize = fileSize;
443
444            // if mimeType was not specified, compute file type based on file extension.
445            if (mMimeType == null) {
446                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
447                if (mediaFileType != null) {
448                    mFileType = mediaFileType.fileType;
449                    mMimeType = mediaFileType.mimeType;
450                }
451            }
452
453            String key = path;
454            if (mCaseInsensitivePaths) {
455                key = path.toLowerCase();
456            }
457            FileCacheEntry entry = mFileCache.get(key);
458            if (entry == null) {
459                entry = new FileCacheEntry(null, 0, path, 0);
460                mFileCache.put(key, entry);
461            }
462            entry.mSeenInFileSystem = true;
463
464            // add some slack to avoid a rounding error
465            long delta = lastModified - entry.mLastModified;
466            if (delta > 1 || delta < -1) {
467                entry.mLastModified = lastModified;
468                entry.mLastModifiedChanged = true;
469            }
470
471            if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
472                mPlayLists.add(entry);
473                // we don't process playlists in the main scan, so return null
474                return null;
475            }
476
477            // clear all the metadata
478            mArtist = null;
479            mAlbumArtist = null;
480            mAlbum = null;
481            mTitle = null;
482            mComposer = null;
483            mGenre = null;
484            mTrack = 0;
485            mYear = 0;
486            mDuration = 0;
487            mPath = path;
488            mLastModified = lastModified;
489            mWriter = null;
490
491            return entry;
492        }
493
494        public void scanFile(String path, long lastModified, long fileSize) {
495            // This is the callback funtion from native codes.
496            // Log.v(TAG, "scanFile: "+path);
497            doScanFile(path, null, lastModified, fileSize, false);
498        }
499
500        public void scanFile(String path, String mimeType, long lastModified, long fileSize) {
501            doScanFile(path, mimeType, lastModified, fileSize, false);
502        }
503
504        public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) {
505            Uri result = null;
506//            long t1 = System.currentTimeMillis();
507            try {
508                FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize);
509                // rescan for metadata if file was modified since last scan
510                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
511                    String lowpath = path.toLowerCase();
512                    boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
513                    boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
514                    boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
515                    boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
516                    boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
517                        (!ringtones && !notifications && !alarms && !podcasts);
518
519                    if (!MediaFile.isImageFileType(mFileType)) {
520                        processFile(path, mimeType, this);
521                    }
522
523                    result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
524                }
525            } catch (RemoteException e) {
526                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
527            }
528//            long t2 = System.currentTimeMillis();
529//            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
530            return result;
531        }
532
533        private int parseSubstring(String s, int start, int defaultValue) {
534            int length = s.length();
535            if (start == length) return defaultValue;
536
537            char ch = s.charAt(start++);
538            // return defaultValue if we have no integer at all
539            if (ch < '0' || ch > '9') return defaultValue;
540
541            int result = ch - '0';
542            while (start < length) {
543                ch = s.charAt(start++);
544                if (ch < '0' || ch > '9') return result;
545                result = result * 10 + (ch - '0');
546            }
547
548            return result;
549        }
550
551        public void handleStringTag(String name, String value) {
552            if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
553                // Don't trim() here, to preserve the special \001 character
554                // used to force sorting. The media provider will trim() before
555                // inserting the title in to the database.
556                mTitle = value;
557            } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
558                mArtist = value.trim();
559            } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")) {
560                mAlbumArtist = value.trim();
561            } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
562                mAlbum = value.trim();
563            } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
564                mComposer = value.trim();
565            } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) {
566                // handle numeric genres, which PV sometimes encodes like "(20)"
567                if (value.length() > 0) {
568                    int genreCode = -1;
569                    char ch = value.charAt(0);
570                    if (ch == '(') {
571                        genreCode = parseSubstring(value, 1, -1);
572                    } else if (ch >= '0' && ch <= '9') {
573                        genreCode = parseSubstring(value, 0, -1);
574                    }
575                    if (genreCode >= 0 && genreCode < ID3_GENRES.length) {
576                        value = ID3_GENRES[genreCode];
577                    } else if (genreCode == 255) {
578                        // 255 is defined to be unknown
579                        value = null;
580                    }
581                }
582                mGenre = value;
583            } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
584                mYear = parseSubstring(value, 0, 0);
585            } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
586                // track number might be of the form "2/12"
587                // we just read the number before the slash
588                int num = parseSubstring(value, 0, 0);
589                mTrack = (mTrack / 1000) * 1000 + num;
590            } else if (name.equalsIgnoreCase("discnumber") ||
591                    name.equals("set") || name.startsWith("set;")) {
592                // set number might be of the form "1/3"
593                // we just read the number before the slash
594                int num = parseSubstring(value, 0, 0);
595                mTrack = (num * 1000) + (mTrack % 1000);
596            } else if (name.equalsIgnoreCase("duration")) {
597                mDuration = parseSubstring(value, 0, 0);
598            } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
599                mWriter = value.trim();
600            }
601        }
602
603        public void setMimeType(String mimeType) {
604            if ("audio/mp4".equals(mMimeType) &&
605                    mimeType.startsWith("video")) {
606                // for feature parity with Donut, we force m4a files to keep the
607                // audio/mp4 mimetype, even if they are really "enhanced podcasts"
608                // with a video track
609                return;
610            }
611            mMimeType = mimeType;
612            mFileType = MediaFile.getFileTypeForMimeType(mimeType);
613        }
614
615        /**
616         * Formats the data into a values array suitable for use with the Media
617         * Content Provider.
618         *
619         * @return a map of values
620         */
621        private ContentValues toValues() {
622            ContentValues map = new ContentValues();
623
624            map.put(MediaStore.MediaColumns.DATA, mPath);
625            map.put(MediaStore.MediaColumns.TITLE, mTitle);
626            map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
627            map.put(MediaStore.MediaColumns.SIZE, mFileSize);
628            map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
629            if (mMtpObjectHandle != 0) {
630                map.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
631            }
632
633            if (MediaFile.isVideoFileType(mFileType)) {
634                map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaStore.UNKNOWN_STRING));
635                map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaStore.UNKNOWN_STRING));
636                map.put(Video.Media.DURATION, mDuration);
637                // FIXME - add RESOLUTION
638            } else if (MediaFile.isImageFileType(mFileType)) {
639                // FIXME - add DESCRIPTION
640            } else if (MediaFile.isAudioFileType(mFileType)) {
641                map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
642                        mArtist : MediaStore.UNKNOWN_STRING);
643                map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
644                        mAlbumArtist.length() > 0) ? mAlbumArtist : null);
645                map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
646                        mAlbum : MediaStore.UNKNOWN_STRING);
647                map.put(Audio.Media.COMPOSER, mComposer);
648                if (mYear != 0) {
649                    map.put(Audio.Media.YEAR, mYear);
650                }
651                map.put(Audio.Media.TRACK, mTrack);
652                map.put(Audio.Media.DURATION, mDuration);
653            }
654            return map;
655        }
656
657        private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,
658                boolean alarms, boolean music, boolean podcasts)
659                throws RemoteException {
660            // update database
661            Uri tableUri;
662            boolean isAudio = MediaFile.isAudioFileType(mFileType);
663            boolean isVideo = MediaFile.isVideoFileType(mFileType);
664            boolean isImage = MediaFile.isImageFileType(mFileType);
665            if (isVideo) {
666                tableUri = mVideoUri;
667            } else if (isImage) {
668                tableUri = mImagesUri;
669            } else if (isAudio) {
670                tableUri = mAudioUri;
671            } else {
672                // don't add file to database if not audio, video or image
673                return null;
674            }
675            entry.mTableUri = tableUri;
676
677             // use album artist if artist is missing
678            if (mArtist == null || mArtist.length() == 0) {
679                mArtist = mAlbumArtist;
680            }
681
682            ContentValues values = toValues();
683            String title = values.getAsString(MediaStore.MediaColumns.TITLE);
684            if (title == null || TextUtils.isEmpty(title.trim())) {
685                title = values.getAsString(MediaStore.MediaColumns.DATA);
686                // extract file name after last slash
687                int lastSlash = title.lastIndexOf('/');
688                if (lastSlash >= 0) {
689                    lastSlash++;
690                    if (lastSlash < title.length()) {
691                        title = title.substring(lastSlash);
692                    }
693                }
694                // truncate the file extension (if any)
695                int lastDot = title.lastIndexOf('.');
696                if (lastDot > 0) {
697                    title = title.substring(0, lastDot);
698                }
699                values.put(MediaStore.MediaColumns.TITLE, title);
700            }
701            String album = values.getAsString(Audio.Media.ALBUM);
702            if (MediaStore.UNKNOWN_STRING.equals(album)) {
703                album = values.getAsString(MediaStore.MediaColumns.DATA);
704                // extract last path segment before file name
705                int lastSlash = album.lastIndexOf('/');
706                if (lastSlash >= 0) {
707                    int previousSlash = 0;
708                    while (true) {
709                        int idx = album.indexOf('/', previousSlash + 1);
710                        if (idx < 0 || idx >= lastSlash) {
711                            break;
712                        }
713                        previousSlash = idx;
714                    }
715                    if (previousSlash != 0) {
716                        album = album.substring(previousSlash + 1, lastSlash);
717                        values.put(Audio.Media.ALBUM, album);
718                    }
719                }
720            }
721            long rowId = entry.mRowId;
722            if (isAudio && rowId == 0) {
723                // Only set these for new entries. For existing entries, they
724                // may have been modified later, and we want to keep the current
725                // values so that custom ringtones still show up in the ringtone
726                // picker.
727                values.put(Audio.Media.IS_RINGTONE, ringtones);
728                values.put(Audio.Media.IS_NOTIFICATION, notifications);
729                values.put(Audio.Media.IS_ALARM, alarms);
730                values.put(Audio.Media.IS_MUSIC, music);
731                values.put(Audio.Media.IS_PODCAST, podcasts);
732            } else if (mFileType == MediaFile.FILE_TYPE_JPEG) {
733                ExifInterface exif = null;
734                try {
735                    exif = new ExifInterface(entry.mPath);
736                } catch (IOException ex) {
737                    // exif is null
738                }
739                if (exif != null) {
740                    float[] latlng = new float[2];
741                    if (exif.getLatLong(latlng)) {
742                        values.put(Images.Media.LATITUDE, latlng[0]);
743                        values.put(Images.Media.LONGITUDE, latlng[1]);
744                    }
745
746                    long time = exif.getGpsDateTime();
747                    if (time != -1) {
748                        values.put(Images.Media.DATE_TAKEN, time);
749                    }
750
751                    int orientation = exif.getAttributeInt(
752                        ExifInterface.TAG_ORIENTATION, -1);
753                    if (orientation != -1) {
754                        // We only recognize a subset of orientation tag values.
755                        int degree;
756                        switch(orientation) {
757                            case ExifInterface.ORIENTATION_ROTATE_90:
758                                degree = 90;
759                                break;
760                            case ExifInterface.ORIENTATION_ROTATE_180:
761                                degree = 180;
762                                break;
763                            case ExifInterface.ORIENTATION_ROTATE_270:
764                                degree = 270;
765                                break;
766                            default:
767                                degree = 0;
768                                break;
769                        }
770                        values.put(Images.Media.ORIENTATION, degree);
771                    }
772                }
773            }
774
775            Uri result = null;
776            if (rowId == 0) {
777                // new file, insert it
778                result = mMediaProvider.insert(tableUri, values);
779                if (result != null) {
780                    rowId = ContentUris.parseId(result);
781                    entry.mRowId = rowId;
782                }
783            } else {
784                // updated file
785                result = ContentUris.withAppendedId(tableUri, rowId);
786                mMediaProvider.update(result, values, null, null);
787            }
788            if (mProcessGenres && mGenre != null) {
789                String genre = mGenre;
790                Uri uri = mGenreCache.get(genre);
791                if (uri == null) {
792                    Cursor cursor = null;
793                    try {
794                        // see if the genre already exists
795                        cursor = mMediaProvider.query(
796                                mGenresUri,
797                                GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?",
798                                        new String[] { genre }, null);
799                        if (cursor == null || cursor.getCount() == 0) {
800                            // genre does not exist, so create the genre in the genre table
801                            values.clear();
802                            values.put(MediaStore.Audio.Genres.NAME, genre);
803                            uri = mMediaProvider.insert(mGenresUri, values);
804                        } else {
805                            // genre already exists, so compute its Uri
806                            cursor.moveToNext();
807                            uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0));
808                        }
809                        if (uri != null) {
810                            uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY);
811                            mGenreCache.put(genre, uri);
812                        }
813                    } finally {
814                        // release the cursor if it exists
815                        if (cursor != null) {
816                            cursor.close();
817                        }
818                    }
819                }
820
821                if (uri != null) {
822                    // add entry to audio_genre_map
823                    values.clear();
824                    values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId));
825                    mMediaProvider.insert(uri, values);
826                }
827            }
828
829            if (notifications && !mDefaultNotificationSet) {
830                if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
831                        doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
832                    setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
833                    mDefaultNotificationSet = true;
834                }
835            } else if (ringtones && !mDefaultRingtoneSet) {
836                if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
837                        doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
838                    setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
839                    mDefaultRingtoneSet = true;
840                }
841            } else if (alarms && !mDefaultAlarmSet) {
842                if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
843                        doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
844                    setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
845                    mDefaultAlarmSet = true;
846                }
847            }
848
849            return result;
850        }
851
852        private boolean doesPathHaveFilename(String path, String filename) {
853            int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
854            int filenameLength = filename.length();
855            return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
856                    pathFilenameStart + filenameLength == path.length();
857        }
858
859        private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
860
861            String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
862                    settingName);
863
864            if (TextUtils.isEmpty(existingSettingValue)) {
865                // Set the setting to the given URI
866                Settings.System.putString(mContext.getContentResolver(), settingName,
867                        ContentUris.withAppendedId(uri, rowId).toString());
868            }
869        }
870
871        public void addNoMediaFolder(String path) {
872            ContentValues values = new ContentValues();
873            values.put(MediaStore.Images.ImageColumns.DATA, "");
874            String [] pathSpec = new String[] {path + '%'};
875            try {
876                // These tables have DELETE_FILE triggers that delete the file from the
877                // sd card when deleting the database entry. We don't want to do this in
878                // this case, since it would cause those files to be removed if a .nomedia
879                // file was added after the fact, when in that case we only want the database
880                // entries to be removed.
881                mMediaProvider.update(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values,
882                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
883                mMediaProvider.update(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values,
884                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
885            } catch (RemoteException e) {
886                throw new RuntimeException();
887            }
888        }
889
890    }; // end of anonymous MediaScannerClient instance
891
892    private void prescan(String filePath) throws RemoteException {
893        Cursor c = null;
894        String where = null;
895        String[] selectionArgs = null;
896
897        if (mFileCache == null) {
898            mFileCache = new HashMap<String, FileCacheEntry>();
899        } else {
900            mFileCache.clear();
901        }
902        if (mPlayLists == null) {
903            mPlayLists = new ArrayList<FileCacheEntry>();
904        } else {
905            mPlayLists.clear();
906        }
907
908        // Build the list of files from the content provider
909        try {
910            // Read existing files from the audio table
911            if (filePath != null) {
912                where = MediaStore.Audio.Media.DATA + "=?";
913                selectionArgs = new String[] { filePath };
914            }
915            c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null);
916
917            if (c != null) {
918                try {
919                    while (c.moveToNext()) {
920                        long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX);
921                        String path = c.getString(PATH_AUDIO_COLUMN_INDEX);
922                        long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX);
923
924                        // Only consider entries with absolute path names.
925                        // This allows storing URIs in the database without the
926                        // media scanner removing them.
927                        if (path.startsWith("/")) {
928                            String key = path;
929                            if (mCaseInsensitivePaths) {
930                                key = path.toLowerCase();
931                            }
932                            mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path,
933                                    lastModified));
934                        }
935                    }
936                } finally {
937                    c.close();
938                    c = null;
939                }
940            }
941
942            // Read existing files from the video table
943            if (filePath != null) {
944                where = MediaStore.Video.Media.DATA + "=?";
945            } else {
946                where = null;
947            }
948            c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null);
949
950            if (c != null) {
951                try {
952                    while (c.moveToNext()) {
953                        long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX);
954                        String path = c.getString(PATH_VIDEO_COLUMN_INDEX);
955                        long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX);
956
957                        // Only consider entries with absolute path names.
958                        // This allows storing URIs in the database without the
959                        // media scanner removing them.
960                        if (path.startsWith("/")) {
961                            String key = path;
962                            if (mCaseInsensitivePaths) {
963                                key = path.toLowerCase();
964                            }
965                            mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path,
966                                    lastModified));
967                        }
968                    }
969                } finally {
970                    c.close();
971                    c = null;
972                }
973            }
974
975            // Read existing files from the images table
976            if (filePath != null) {
977                where = MediaStore.Images.Media.DATA + "=?";
978            } else {
979                where = null;
980            }
981            mOriginalCount = 0;
982            c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null);
983
984            if (c != null) {
985                try {
986                    mOriginalCount = c.getCount();
987                    while (c.moveToNext()) {
988                        long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX);
989                        String path = c.getString(PATH_IMAGES_COLUMN_INDEX);
990                       long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX);
991
992                       // Only consider entries with absolute path names.
993                       // This allows storing URIs in the database without the
994                       // media scanner removing them.
995                       if (path.startsWith("/")) {
996                           String key = path;
997                           if (mCaseInsensitivePaths) {
998                               key = path.toLowerCase();
999                           }
1000                           mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path,
1001                                   lastModified));
1002                       }
1003                    }
1004                } finally {
1005                    c.close();
1006                    c = null;
1007                }
1008            }
1009
1010            if (mProcessPlaylists) {
1011                // Read existing files from the playlists table
1012                if (filePath != null) {
1013                    where = MediaStore.Audio.Playlists.DATA + "=?";
1014                } else {
1015                    where = null;
1016                }
1017                c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null);
1018
1019                if (c != null) {
1020                    try {
1021                        while (c.moveToNext()) {
1022                            String path = c.getString(PATH_PLAYLISTS_COLUMN_INDEX);
1023
1024                            if (path != null && path.length() > 0) {
1025                                long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX);
1026                                long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX);
1027
1028                                String key = path;
1029                                if (mCaseInsensitivePaths) {
1030                                    key = path.toLowerCase();
1031                                }
1032                                mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path,
1033                                        lastModified));
1034                            }
1035                        }
1036                    } finally {
1037                        c.close();
1038                        c = null;
1039                    }
1040                }
1041            }
1042        }
1043        finally {
1044            if (c != null) {
1045                c.close();
1046            }
1047        }
1048    }
1049
1050    private boolean inScanDirectory(String path, String[] directories) {
1051        for (int i = 0; i < directories.length; i++) {
1052            if (path.startsWith(directories[i])) {
1053                return true;
1054            }
1055        }
1056        return false;
1057    }
1058
1059    private void pruneDeadThumbnailFiles() {
1060        HashSet<String> existingFiles = new HashSet<String>();
1061        String directory = "/sdcard/DCIM/.thumbnails";
1062        String [] files = (new File(directory)).list();
1063        if (files == null)
1064            files = new String[0];
1065
1066        for (int i = 0; i < files.length; i++) {
1067            String fullPathString = directory + "/" + files[i];
1068            existingFiles.add(fullPathString);
1069        }
1070
1071        try {
1072            Cursor c = mMediaProvider.query(
1073                    mThumbsUri,
1074                    new String [] { "_data" },
1075                    null,
1076                    null,
1077                    null);
1078            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
1079            if (c != null && c.moveToFirst()) {
1080                do {
1081                    String fullPathString = c.getString(0);
1082                    existingFiles.remove(fullPathString);
1083                } while (c.moveToNext());
1084            }
1085
1086            for (String fileToDelete : existingFiles) {
1087                if (Config.LOGV)
1088                    Log.v(TAG, "fileToDelete is " + fileToDelete);
1089                try {
1090                    (new File(fileToDelete)).delete();
1091                } catch (SecurityException ex) {
1092                }
1093            }
1094
1095            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
1096            if (c != null) {
1097                c.close();
1098            }
1099        } catch (RemoteException e) {
1100            // We will soon be killed...
1101        }
1102    }
1103
1104    private void postscan(String[] directories) throws RemoteException {
1105        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1106
1107        while (iterator.hasNext()) {
1108            FileCacheEntry entry = iterator.next();
1109            String path = entry.mPath;
1110
1111            // remove database entries for files that no longer exist.
1112            boolean fileMissing = false;
1113
1114            if (!entry.mSeenInFileSystem) {
1115                if (inScanDirectory(path, directories)) {
1116                    // we didn't see this file in the scan directory.
1117                    fileMissing = true;
1118                } else {
1119                    // the file is outside of our scan directory,
1120                    // so we need to check for file existence here.
1121                    File testFile = new File(path);
1122                    if (!testFile.exists()) {
1123                        fileMissing = true;
1124                    }
1125                }
1126            }
1127
1128            if (fileMissing) {
1129                // do not delete missing playlists, since they may have been modified by the user.
1130                // the user can delete them in the media player instead.
1131                // instead, clear the path and lastModified fields in the row
1132                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1133                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1134
1135                if (MediaFile.isPlayListFileType(fileType)) {
1136                    ContentValues values = new ContentValues();
1137                    values.put(MediaStore.Audio.Playlists.DATA, "");
1138                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0);
1139                    mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null);
1140                } else {
1141                    mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null);
1142                    iterator.remove();
1143                }
1144            }
1145        }
1146
1147        // handle playlists last, after we know what media files are on the storage.
1148        if (mProcessPlaylists) {
1149            processPlayLists();
1150        }
1151
1152        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1153            pruneDeadThumbnailFiles();
1154
1155        // allow GC to clean up
1156        mGenreCache = null;
1157        mPlayLists = null;
1158        mFileCache = null;
1159        mMediaProvider = null;
1160    }
1161
1162    private void initialize(String volumeName) {
1163        mMediaProvider = mContext.getContentResolver().acquireProvider("media");
1164
1165        mAudioUri = Audio.Media.getContentUri(volumeName);
1166        mVideoUri = Video.Media.getContentUri(volumeName);
1167        mImagesUri = Images.Media.getContentUri(volumeName);
1168        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
1169
1170        if (!volumeName.equals("internal")) {
1171            // we only support playlists on external media
1172            mProcessPlaylists = true;
1173            mProcessGenres = true;
1174            mGenreCache = new HashMap<String, Uri>();
1175            mGenresUri = Genres.getContentUri(volumeName);
1176            mPlaylistsUri = Playlists.getContentUri(volumeName);
1177            // assuming external storage is FAT (case insensitive), except on the simulator.
1178            if ( Process.supportsProcesses()) {
1179                mCaseInsensitivePaths = true;
1180            }
1181        }
1182    }
1183
1184    public void scanDirectories(String[] directories, String volumeName) {
1185        try {
1186            long start = System.currentTimeMillis();
1187            initialize(volumeName);
1188            prescan(null);
1189            long prescan = System.currentTimeMillis();
1190
1191            for (int i = 0; i < directories.length; i++) {
1192                processDirectory(directories[i], MediaFile.sFileExtensions, mClient);
1193            }
1194            long scan = System.currentTimeMillis();
1195            postscan(directories);
1196            long end = System.currentTimeMillis();
1197
1198            if (Config.LOGD) {
1199                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1200                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1201                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1202                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1203            }
1204        } catch (SQLException e) {
1205            // this might happen if the SD card is removed while the media scanner is running
1206            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1207        } catch (UnsupportedOperationException e) {
1208            // this might happen if the SD card is removed while the media scanner is running
1209            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1210        } catch (RemoteException e) {
1211            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1212        }
1213    }
1214
1215    // this function is used to scan a single file
1216    public Uri scanSingleFile(String path, String volumeName, String mimeType) {
1217        try {
1218            initialize(volumeName);
1219            prescan(path);
1220
1221            File file = new File(path);
1222
1223            // lastModified is in milliseconds on Files.
1224            long lastModifiedSeconds = file.lastModified() / 1000;
1225
1226            // always scan the file, so we can return the content://media Uri for existing files
1227            return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), true);
1228        } catch (RemoteException e) {
1229            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1230            return null;
1231        }
1232    }
1233
1234    public Uri scanMtpFile(String path, String volumeName, int objectHandle, int format) {
1235        String mimeType = MediaFile.getMimeTypeForFormatCode(format);
1236        mMtpObjectHandle = objectHandle;
1237        Uri result = scanSingleFile(path, volumeName, mimeType);
1238        mMtpObjectHandle = 0;
1239        return result;
1240    }
1241
1242    // returns the number of matching file/directory names, starting from the right
1243    private int matchPaths(String path1, String path2) {
1244        int result = 0;
1245        int end1 = path1.length();
1246        int end2 = path2.length();
1247
1248        while (end1 > 0 && end2 > 0) {
1249            int slash1 = path1.lastIndexOf('/', end1 - 1);
1250            int slash2 = path2.lastIndexOf('/', end2 - 1);
1251            int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1252            int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1253            int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1254            int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1255            if (start1 < 0) start1 = 0; else start1++;
1256            if (start2 < 0) start2 = 0; else start2++;
1257            int length = end1 - start1;
1258            if (end2 - start2 != length) break;
1259            if (path1.regionMatches(true, start1, path2, start2, length)) {
1260                result++;
1261                end1 = start1 - 1;
1262                end2 = start2 - 1;
1263            } else break;
1264        }
1265
1266        return result;
1267    }
1268
1269    private boolean addPlayListEntry(String entry, String playListDirectory,
1270            Uri uri, ContentValues values, int index) {
1271
1272        // watch for trailing whitespace
1273        int entryLength = entry.length();
1274        while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--;
1275        // path should be longer than 3 characters.
1276        // avoid index out of bounds errors below by returning here.
1277        if (entryLength < 3) return false;
1278        if (entryLength < entry.length()) entry = entry.substring(0, entryLength);
1279
1280        // does entry appear to be an absolute path?
1281        // look for Unix or DOS absolute paths
1282        char ch1 = entry.charAt(0);
1283        boolean fullPath = (ch1 == '/' ||
1284                (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\'));
1285        // if we have a relative path, combine entry with playListDirectory
1286        if (!fullPath)
1287            entry = playListDirectory + entry;
1288
1289        //FIXME - should we look for "../" within the path?
1290
1291        // best matching MediaFile for the play list entry
1292        FileCacheEntry bestMatch = null;
1293
1294        // number of rightmost file/directory names for bestMatch
1295        int bestMatchLength = 0;
1296
1297        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1298        while (iterator.hasNext()) {
1299            FileCacheEntry cacheEntry = iterator.next();
1300            String path = cacheEntry.mPath;
1301
1302            if (path.equalsIgnoreCase(entry)) {
1303                bestMatch = cacheEntry;
1304                break;    // don't bother continuing search
1305            }
1306
1307            int matchLength = matchPaths(path, entry);
1308            if (matchLength > bestMatchLength) {
1309                bestMatch = cacheEntry;
1310                bestMatchLength = matchLength;
1311            }
1312        }
1313
1314        // if the match is not for an audio file, bail out
1315        if (bestMatch == null || ! mAudioUri.equals(bestMatch.mTableUri)) {
1316            return false;
1317        }
1318
1319        try {
1320        // OK, now we need to add this to the database
1321            values.clear();
1322            values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1323            values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId));
1324            mMediaProvider.insert(uri, values);
1325        } catch (RemoteException e) {
1326            Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e);
1327            return false;
1328        }
1329
1330        return true;
1331    }
1332
1333    private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1334        BufferedReader reader = null;
1335        try {
1336            File f = new File(path);
1337            if (f.exists()) {
1338                reader = new BufferedReader(
1339                        new InputStreamReader(new FileInputStream(f)), 8192);
1340                String line = reader.readLine();
1341                int index = 0;
1342                while (line != null) {
1343                    // ignore comment lines, which begin with '#'
1344                    if (line.length() > 0 && line.charAt(0) != '#') {
1345                        values.clear();
1346                        if (addPlayListEntry(line, playListDirectory, uri, values, index))
1347                            index++;
1348                    }
1349                    line = reader.readLine();
1350                }
1351            }
1352        } catch (IOException e) {
1353            Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1354        } finally {
1355            try {
1356                if (reader != null)
1357                    reader.close();
1358            } catch (IOException e) {
1359                Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1360            }
1361        }
1362    }
1363
1364    private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1365        BufferedReader reader = null;
1366        try {
1367            File f = new File(path);
1368            if (f.exists()) {
1369                reader = new BufferedReader(
1370                        new InputStreamReader(new FileInputStream(f)), 8192);
1371                String line = reader.readLine();
1372                int index = 0;
1373                while (line != null) {
1374                    // ignore comment lines, which begin with '#'
1375                    if (line.startsWith("File")) {
1376                        int equals = line.indexOf('=');
1377                        if (equals > 0) {
1378                            values.clear();
1379                            if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index))
1380                                index++;
1381                        }
1382                    }
1383                    line = reader.readLine();
1384                }
1385            }
1386        } catch (IOException e) {
1387            Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1388        } finally {
1389            try {
1390                if (reader != null)
1391                    reader.close();
1392            } catch (IOException e) {
1393                Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1394            }
1395        }
1396    }
1397
1398    class WplHandler implements ElementListener {
1399
1400        final ContentHandler handler;
1401        String playListDirectory;
1402        Uri uri;
1403        ContentValues values = new ContentValues();
1404        int index = 0;
1405
1406        public WplHandler(String playListDirectory, Uri uri) {
1407            this.playListDirectory = playListDirectory;
1408            this.uri = uri;
1409
1410            RootElement root = new RootElement("smil");
1411            Element body = root.getChild("body");
1412            Element seq = body.getChild("seq");
1413            Element media = seq.getChild("media");
1414            media.setElementListener(this);
1415
1416            this.handler = root.getContentHandler();
1417        }
1418
1419        public void start(Attributes attributes) {
1420            String path = attributes.getValue("", "src");
1421            if (path != null) {
1422                values.clear();
1423                if (addPlayListEntry(path, playListDirectory, uri, values, index)) {
1424                    index++;
1425                }
1426            }
1427        }
1428
1429       public void end() {
1430       }
1431
1432        ContentHandler getContentHandler() {
1433            return handler;
1434        }
1435    }
1436
1437    private void processWplPlayList(String path, String playListDirectory, Uri uri) {
1438        FileInputStream fis = null;
1439        try {
1440            File f = new File(path);
1441            if (f.exists()) {
1442                fis = new FileInputStream(f);
1443
1444                Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler());
1445            }
1446        } catch (SAXException e) {
1447            e.printStackTrace();
1448        } catch (IOException e) {
1449            e.printStackTrace();
1450        } finally {
1451            try {
1452                if (fis != null)
1453                    fis.close();
1454            } catch (IOException e) {
1455                Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1456            }
1457        }
1458    }
1459
1460    private void processPlayLists() throws RemoteException {
1461        Iterator<FileCacheEntry> iterator = mPlayLists.iterator();
1462        while (iterator.hasNext()) {
1463            FileCacheEntry entry = iterator.next();
1464            String path = entry.mPath;
1465
1466            // only process playlist files if they are new or have been modified since the last scan
1467            if (entry.mLastModifiedChanged) {
1468                ContentValues values = new ContentValues();
1469                int lastSlash = path.lastIndexOf('/');
1470                if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1471                Uri uri, membersUri;
1472                long rowId = entry.mRowId;
1473                if (rowId == 0) {
1474                    // Create a new playlist
1475
1476                    int lastDot = path.lastIndexOf('.');
1477                    String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot));
1478                    values.put(MediaStore.Audio.Playlists.NAME, name);
1479                    values.put(MediaStore.Audio.Playlists.DATA, path);
1480                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1481                    uri = mMediaProvider.insert(mPlaylistsUri, values);
1482                    rowId = ContentUris.parseId(uri);
1483                    membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1484                } else {
1485                    uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1486
1487                    // update lastModified value of existing playlist
1488                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1489                    mMediaProvider.update(uri, values, null, null);
1490
1491                    // delete members of existing playlist
1492                    membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1493                    mMediaProvider.delete(membersUri, null, null);
1494                }
1495
1496                String playListDirectory = path.substring(0, lastSlash + 1);
1497                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1498                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1499
1500                if (fileType == MediaFile.FILE_TYPE_M3U)
1501                    processM3uPlayList(path, playListDirectory, membersUri, values);
1502                else if (fileType == MediaFile.FILE_TYPE_PLS)
1503                    processPlsPlayList(path, playListDirectory, membersUri, values);
1504                else if (fileType == MediaFile.FILE_TYPE_WPL)
1505                    processWplPlayList(path, playListDirectory, membersUri);
1506
1507                Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null,
1508                        null, null);
1509                try {
1510                    if (cursor == null || cursor.getCount() == 0) {
1511                        Log.d(TAG, "playlist is empty - deleting");
1512                        mMediaProvider.delete(uri, null, null);
1513                    }
1514                } finally {
1515                    if (cursor != null) cursor.close();
1516                }
1517            }
1518        }
1519    }
1520
1521    private native void processDirectory(String path, String extensions, MediaScannerClient client);
1522    private native void processFile(String path, String mimeType, MediaScannerClient client);
1523    public native void setLocale(String locale);
1524
1525    public native byte[] extractAlbumArt(FileDescriptor fd);
1526
1527    private static native final void native_init();
1528    private native final void native_setup();
1529    private native final void native_finalize();
1530    @Override
1531    protected void finalize() {
1532        mContext.getContentResolver().releaseProvider(mMediaProvider);
1533        native_finalize();
1534    }
1535}
1536