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