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