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