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