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