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