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