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