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