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