MediaScanner.java revision 076e05b488e40fdd946f0d35137fe66a576efe09
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,
411                long fileSize, boolean isDirectory) {
412            mMimeType = mimeType;
413            mFileType = 0;
414            mFileSize = fileSize;
415
416            if (!isDirectory) {
417                // special case certain file names
418                // I use regionMatches() instead of substring() below
419                // to avoid memory allocation
420                int lastSlash = path.lastIndexOf('/');
421                if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
422                    // ignore those ._* files created by MacOS
423                    if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
424                        return null;
425                    }
426
427                    // ignore album art files created by Windows Media Player:
428                    // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
429                    // and AlbumArt_{...}_Small.jpg
430                    if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
431                        if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
432                                path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
433                            return null;
434                        }
435                        int length = path.length() - lastSlash - 1;
436                        if ((length == 17 && path.regionMatches(
437                                true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
438                                (length == 10
439                                 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
440                            return null;
441                        }
442                    }
443                }
444
445                // try mimeType first, if it is specified
446                if (mimeType != null) {
447                    mFileType = MediaFile.getFileTypeForMimeType(mimeType);
448                }
449
450                // if mimeType was not specified, compute file type based on file extension.
451                if (mFileType == 0) {
452                    MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
453                    if (mediaFileType != null) {
454                        mFileType = mediaFileType.fileType;
455                        if (mMimeType == null) {
456                            mMimeType = mediaFileType.mimeType;
457                        }
458                    }
459                }
460
461                if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
462                    mFileType = getFileTypeFromDrm(path);
463                }
464            }
465
466            String key = path;
467            if (mMediaStoragePath != null && key.startsWith(mMediaStoragePath)) {
468                // MediaProvider uses external variant of path for _data, so we need to match
469                // against that path instead.
470                key = mExternalStoragePath + key.substring(mMediaStoragePath.length());
471            }
472            if (mCaseInsensitivePaths) {
473                key = path.toLowerCase();
474            }
475            FileCacheEntry entry = mFileCache.get(key);
476            if (entry == null) {
477                Uri tableUri;
478                if (isDirectory) {
479                    tableUri = mFilesUri;
480                } else if (MediaFile.isVideoFileType(mFileType)) {
481                    tableUri = mVideoUri;
482                } else if (MediaFile.isImageFileType(mFileType)) {
483                    tableUri = mImagesUri;
484                } else if (MediaFile.isAudioFileType(mFileType)) {
485                    tableUri = mAudioUri;
486                } else {
487                    tableUri = mFilesUri;
488                }
489                entry = new FileCacheEntry(tableUri, 0, path, 0,
490                        (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
491                mFileCache.put(key, entry);
492            }
493            entry.mSeenInFileSystem = true;
494
495            // add some slack to avoid a rounding error
496            long delta = lastModified - entry.mLastModified;
497            if (delta > 1 || delta < -1) {
498                entry.mLastModified = lastModified;
499                entry.mLastModifiedChanged = true;
500            }
501
502            if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
503                mPlayLists.add(entry);
504                // we don't process playlists in the main scan, so return null
505                return null;
506            }
507
508            // clear all the metadata
509            mArtist = null;
510            mAlbumArtist = null;
511            mAlbum = null;
512            mTitle = null;
513            mComposer = null;
514            mGenre = null;
515            mTrack = 0;
516            mYear = 0;
517            mDuration = 0;
518            mPath = path;
519            mLastModified = lastModified;
520            mWriter = null;
521
522            return entry;
523        }
524
525        public void scanFile(String path, long lastModified, long fileSize, boolean isDirectory) {
526            // This is the callback funtion from native codes.
527            // Log.v(TAG, "scanFile: "+path);
528            doScanFile(path, null, lastModified, fileSize, isDirectory, false);
529        }
530
531        public Uri doScanFile(String path, String mimeType, long lastModified,
532                long fileSize, boolean isDirectory, boolean scanAlways) {
533            Uri result = null;
534//            long t1 = System.currentTimeMillis();
535            try {
536                FileCacheEntry entry = beginFile(path, mimeType, lastModified,
537                        fileSize, isDirectory);
538                // rescan for metadata if file was modified since last scan
539                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
540                    String lowpath = path.toLowerCase();
541                    boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
542                    boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
543                    boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
544                    boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
545                    boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
546                        (!ringtones && !notifications && !alarms && !podcasts);
547
548                    // we only extract metadata for audio and video files
549                    if (MediaFile.isAudioFileType(mFileType)
550                            || MediaFile.isVideoFileType(mFileType)) {
551                        processFile(path, mimeType, this);
552                    }
553
554                    result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
555                }
556            } catch (RemoteException e) {
557                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
558            }
559//            long t2 = System.currentTimeMillis();
560//            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
561            return result;
562        }
563
564        private int parseSubstring(String s, int start, int defaultValue) {
565            int length = s.length();
566            if (start == length) return defaultValue;
567
568            char ch = s.charAt(start++);
569            // return defaultValue if we have no integer at all
570            if (ch < '0' || ch > '9') return defaultValue;
571
572            int result = ch - '0';
573            while (start < length) {
574                ch = s.charAt(start++);
575                if (ch < '0' || ch > '9') return result;
576                result = result * 10 + (ch - '0');
577            }
578
579            return result;
580        }
581
582        public void handleStringTag(String name, String value) {
583            if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
584                // Don't trim() here, to preserve the special \001 character
585                // used to force sorting. The media provider will trim() before
586                // inserting the title in to the database.
587                mTitle = value;
588            } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
589                mArtist = value.trim();
590            } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
591                    || name.equalsIgnoreCase("band") || name.startsWith("band;")) {
592                mAlbumArtist = value.trim();
593            } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
594                mAlbum = value.trim();
595            } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
596                mComposer = value.trim();
597            } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) {
598                // handle numeric genres, which PV sometimes encodes like "(20)"
599                if (value.length() > 0) {
600                    int genreCode = -1;
601                    char ch = value.charAt(0);
602                    if (ch == '(') {
603                        genreCode = parseSubstring(value, 1, -1);
604                    } else if (ch >= '0' && ch <= '9') {
605                        genreCode = parseSubstring(value, 0, -1);
606                    }
607                    if (genreCode >= 0 && genreCode < ID3_GENRES.length) {
608                        value = ID3_GENRES[genreCode];
609                    } else if (genreCode == 255) {
610                        // 255 is defined to be unknown
611                        value = null;
612                    }
613                }
614                mGenre = value;
615            } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
616                mYear = parseSubstring(value, 0, 0);
617            } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
618                // track number might be of the form "2/12"
619                // we just read the number before the slash
620                int num = parseSubstring(value, 0, 0);
621                mTrack = (mTrack / 1000) * 1000 + num;
622            } else if (name.equalsIgnoreCase("discnumber") ||
623                    name.equals("set") || name.startsWith("set;")) {
624                // set number might be of the form "1/3"
625                // we just read the number before the slash
626                int num = parseSubstring(value, 0, 0);
627                mTrack = (num * 1000) + (mTrack % 1000);
628            } else if (name.equalsIgnoreCase("duration")) {
629                mDuration = parseSubstring(value, 0, 0);
630            } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
631                mWriter = value.trim();
632            }
633        }
634
635        public void setMimeType(String mimeType) {
636            if ("audio/mp4".equals(mMimeType) &&
637                    mimeType.startsWith("video")) {
638                // for feature parity with Donut, we force m4a files to keep the
639                // audio/mp4 mimetype, even if they are really "enhanced podcasts"
640                // with a video track
641                return;
642            }
643            mMimeType = mimeType;
644            mFileType = MediaFile.getFileTypeForMimeType(mimeType);
645        }
646
647        /**
648         * Formats the data into a values array suitable for use with the Media
649         * Content Provider.
650         *
651         * @return a map of values
652         */
653        private ContentValues toValues() {
654            ContentValues map = new ContentValues();
655
656            map.put(MediaStore.MediaColumns.DATA, mPath);
657            map.put(MediaStore.MediaColumns.TITLE, mTitle);
658            map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
659            map.put(MediaStore.MediaColumns.SIZE, mFileSize);
660            map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
661
662            if (MediaFile.isVideoFileType(mFileType)) {
663                map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaStore.UNKNOWN_STRING));
664                map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaStore.UNKNOWN_STRING));
665                map.put(Video.Media.DURATION, mDuration);
666                // FIXME - add RESOLUTION
667            } else if (MediaFile.isImageFileType(mFileType)) {
668                // FIXME - add DESCRIPTION
669            } else if (MediaFile.isAudioFileType(mFileType)) {
670                map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
671                        mArtist : MediaStore.UNKNOWN_STRING);
672                map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
673                        mAlbumArtist.length() > 0) ? mAlbumArtist : null);
674                map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
675                        mAlbum : MediaStore.UNKNOWN_STRING);
676                map.put(Audio.Media.COMPOSER, mComposer);
677                if (mYear != 0) {
678                    map.put(Audio.Media.YEAR, mYear);
679                }
680                map.put(Audio.Media.TRACK, mTrack);
681                map.put(Audio.Media.DURATION, mDuration);
682            }
683            return map;
684        }
685
686        private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,
687                boolean alarms, boolean music, boolean podcasts)
688                throws RemoteException {
689            // update database
690
691             // use album artist if artist is missing
692            if (mArtist == null || mArtist.length() == 0) {
693                mArtist = mAlbumArtist;
694            }
695
696            ContentValues values = toValues();
697            String title = values.getAsString(MediaStore.MediaColumns.TITLE);
698            if (title == null || TextUtils.isEmpty(title.trim())) {
699                title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
700                values.put(MediaStore.MediaColumns.TITLE, title);
701            }
702            String album = values.getAsString(Audio.Media.ALBUM);
703            if (MediaStore.UNKNOWN_STRING.equals(album)) {
704                album = values.getAsString(MediaStore.MediaColumns.DATA);
705                // extract last path segment before file name
706                int lastSlash = album.lastIndexOf('/');
707                if (lastSlash >= 0) {
708                    int previousSlash = 0;
709                    while (true) {
710                        int idx = album.indexOf('/', previousSlash + 1);
711                        if (idx < 0 || idx >= lastSlash) {
712                            break;
713                        }
714                        previousSlash = idx;
715                    }
716                    if (previousSlash != 0) {
717                        album = album.substring(previousSlash + 1, lastSlash);
718                        values.put(Audio.Media.ALBUM, album);
719                    }
720                }
721            }
722            long rowId = entry.mRowId;
723            if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
724                // Only set these for new entries. For existing entries, they
725                // may have been modified later, and we want to keep the current
726                // values so that custom ringtones still show up in the ringtone
727                // picker.
728                values.put(Audio.Media.IS_RINGTONE, ringtones);
729                values.put(Audio.Media.IS_NOTIFICATION, notifications);
730                values.put(Audio.Media.IS_ALARM, alarms);
731                values.put(Audio.Media.IS_MUSIC, music);
732                values.put(Audio.Media.IS_PODCAST, podcasts);
733            } else if (mFileType == MediaFile.FILE_TYPE_JPEG) {
734                ExifInterface exif = null;
735                try {
736                    exif = new ExifInterface(entry.mPath);
737                } catch (IOException ex) {
738                    // exif is null
739                }
740                if (exif != null) {
741                    float[] latlng = new float[2];
742                    if (exif.getLatLong(latlng)) {
743                        values.put(Images.Media.LATITUDE, latlng[0]);
744                        values.put(Images.Media.LONGITUDE, latlng[1]);
745                    }
746
747                    long time = exif.getGpsDateTime();
748                    if (time != -1) {
749                        values.put(Images.Media.DATE_TAKEN, time);
750                    }
751
752                    int orientation = exif.getAttributeInt(
753                        ExifInterface.TAG_ORIENTATION, -1);
754                    if (orientation != -1) {
755                        // We only recognize a subset of orientation tag values.
756                        int degree;
757                        switch(orientation) {
758                            case ExifInterface.ORIENTATION_ROTATE_90:
759                                degree = 90;
760                                break;
761                            case ExifInterface.ORIENTATION_ROTATE_180:
762                                degree = 180;
763                                break;
764                            case ExifInterface.ORIENTATION_ROTATE_270:
765                                degree = 270;
766                                break;
767                            default:
768                                degree = 0;
769                                break;
770                        }
771                        values.put(Images.Media.ORIENTATION, degree);
772                    }
773                }
774            }
775
776            Uri tableUri = entry.mTableUri;
777            Uri result = null;
778            if (rowId == 0) {
779                if (mMtpObjectHandle != 0) {
780                    values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
781                }
782                if (tableUri == mFilesUri) {
783                    int format = entry.mFormat;
784                    if (format == 0) {
785                        format = MediaFile.getFormatCode(entry.mPath, mMimeType);
786                    }
787                    values.put(Files.FileColumns.FORMAT, format);
788                }
789                // new file, insert it
790                result = mMediaProvider.insert(tableUri, values);
791                if (result != null) {
792                    rowId = ContentUris.parseId(result);
793                    entry.mRowId = rowId;
794                }
795            } else {
796                // updated file
797                result = ContentUris.withAppendedId(tableUri, rowId);
798                mMediaProvider.update(result, values, null, null);
799            }
800            if (mProcessGenres && mGenre != null) {
801                String genre = mGenre;
802                Uri uri = mGenreCache.get(genre);
803                if (uri == null) {
804                    Cursor cursor = null;
805                    try {
806                        // see if the genre already exists
807                        cursor = mMediaProvider.query(
808                                mGenresUri,
809                                GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?",
810                                        new String[] { genre }, null);
811                        if (cursor == null || cursor.getCount() == 0) {
812                            // genre does not exist, so create the genre in the genre table
813                            values.clear();
814                            values.put(MediaStore.Audio.Genres.NAME, genre);
815                            uri = mMediaProvider.insert(mGenresUri, values);
816                        } else {
817                            // genre already exists, so compute its Uri
818                            cursor.moveToNext();
819                            uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0));
820                        }
821                        if (uri != null) {
822                            uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY);
823                            mGenreCache.put(genre, uri);
824                        }
825                    } finally {
826                        // release the cursor if it exists
827                        if (cursor != null) {
828                            cursor.close();
829                        }
830                    }
831                }
832
833                if (uri != null) {
834                    // add entry to audio_genre_map
835                    values.clear();
836                    values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId));
837                    mMediaProvider.insert(uri, values);
838                }
839            }
840
841            if (notifications && !mDefaultNotificationSet) {
842                if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
843                        doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
844                    setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
845                    mDefaultNotificationSet = true;
846                }
847            } else if (ringtones && !mDefaultRingtoneSet) {
848                if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
849                        doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
850                    setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
851                    mDefaultRingtoneSet = true;
852                }
853            } else if (alarms && !mDefaultAlarmSet) {
854                if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
855                        doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
856                    setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
857                    mDefaultAlarmSet = true;
858                }
859            }
860
861            return result;
862        }
863
864        private boolean doesPathHaveFilename(String path, String filename) {
865            int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
866            int filenameLength = filename.length();
867            return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
868                    pathFilenameStart + filenameLength == path.length();
869        }
870
871        private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
872
873            String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
874                    settingName);
875
876            if (TextUtils.isEmpty(existingSettingValue)) {
877                // Set the setting to the given URI
878                Settings.System.putString(mContext.getContentResolver(), settingName,
879                        ContentUris.withAppendedId(uri, rowId).toString());
880            }
881        }
882
883        public void addNoMediaFolder(String path) {
884            ContentValues values = new ContentValues();
885            values.put(MediaStore.Images.ImageColumns.DATA, "");
886            String [] pathSpec = new String[] {path + '%'};
887            try {
888                // These tables have DELETE_FILE triggers that delete the file from the
889                // sd card when deleting the database entry. We don't want to do this in
890                // this case, since it would cause those files to be removed if a .nomedia
891                // file was added after the fact, when in that case we only want the database
892                // entries to be removed.
893                mMediaProvider.update(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values,
894                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
895                mMediaProvider.update(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values,
896                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
897            } catch (RemoteException e) {
898                throw new RuntimeException();
899            }
900        }
901
902        private int getFileTypeFromDrm(String path) {
903            if (!isDrmEnabled()) {
904                return 0;
905            }
906
907            int resultFileType = 0;
908
909            if (mDrmManagerClient == null) {
910                mDrmManagerClient = new DrmManagerClient(mContext);
911            }
912
913            if (mDrmManagerClient.canHandle(path, null)) {
914                String drmMimetype = mDrmManagerClient.getOriginalMimeType(path);
915                if (drmMimetype != null) {
916                    mMimeType = drmMimetype;
917                    resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype);
918                }
919            }
920            return resultFileType;
921        }
922
923    }; // end of anonymous MediaScannerClient instance
924
925    private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
926        Cursor c = null;
927        String where = null;
928        String[] selectionArgs = null;
929
930        if (mFileCache == null) {
931            mFileCache = new HashMap<String, FileCacheEntry>();
932        } else {
933            mFileCache.clear();
934        }
935        if (mPlayLists == null) {
936            mPlayLists = new ArrayList<FileCacheEntry>();
937        } else {
938            mPlayLists.clear();
939        }
940
941        if (filePath != null) {
942            if (mMediaStoragePath != null && filePath.startsWith(mMediaStoragePath)) {
943                // MediaProvider uses external variant of path for _data, so we need to query
944                // using that path instead.
945                filePath = mExternalStoragePath + filePath.substring(mMediaStoragePath.length());
946            }
947
948            // query for only one file
949            where = Files.FileColumns.DATA + "=?";
950            selectionArgs = new String[] { filePath };
951        }
952
953        // Build the list of files from the content provider
954        try {
955            if (prescanFiles) {
956                // First read existing files from the files table
957
958                c = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION,
959                        where, selectionArgs, null);
960
961                if (c != null) {
962                    while (c.moveToNext()) {
963                        long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
964                        String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
965                        int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
966                        long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
967
968                        // Only consider entries with absolute path names.
969                        // This allows storing URIs in the database without the
970                        // media scanner removing them.
971                        if (path.startsWith("/")) {
972                            String key = path;
973                            if (mCaseInsensitivePaths) {
974                                key = path.toLowerCase();
975                            }
976
977                            FileCacheEntry entry = new FileCacheEntry(mFilesUri, rowId, path,
978                                    lastModified, format);
979                            mFileCache.put(key, entry);
980                        }
981                    }
982                    c.close();
983                    c = null;
984                }
985            }
986        }
987        finally {
988            if (c != null) {
989                c.close();
990            }
991        }
992
993        // compute original size of images
994        mOriginalCount = 0;
995        c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null);
996        if (c != null) {
997            mOriginalCount = c.getCount();
998            c.close();
999        }
1000    }
1001
1002    private boolean inScanDirectory(String path, String[] directories) {
1003        for (int i = 0; i < directories.length; i++) {
1004            String directory = directories[i];
1005            if (mExternalStoragePath != null && directory.equals(mMediaStoragePath)) {
1006                // database paths use external storage prefix
1007                directory = mExternalStoragePath;
1008            }
1009            if (path.startsWith(directory)) {
1010                return true;
1011            }
1012        }
1013        return false;
1014    }
1015
1016    private void pruneDeadThumbnailFiles() {
1017        HashSet<String> existingFiles = new HashSet<String>();
1018        String directory = "/sdcard/DCIM/.thumbnails";
1019        String [] files = (new File(directory)).list();
1020        if (files == null)
1021            files = new String[0];
1022
1023        for (int i = 0; i < files.length; i++) {
1024            String fullPathString = directory + "/" + files[i];
1025            existingFiles.add(fullPathString);
1026        }
1027
1028        try {
1029            Cursor c = mMediaProvider.query(
1030                    mThumbsUri,
1031                    new String [] { "_data" },
1032                    null,
1033                    null,
1034                    null);
1035            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
1036            if (c != null && c.moveToFirst()) {
1037                do {
1038                    String fullPathString = c.getString(0);
1039                    existingFiles.remove(fullPathString);
1040                } while (c.moveToNext());
1041            }
1042
1043            for (String fileToDelete : existingFiles) {
1044                if (Config.LOGV)
1045                    Log.v(TAG, "fileToDelete is " + fileToDelete);
1046                try {
1047                    (new File(fileToDelete)).delete();
1048                } catch (SecurityException ex) {
1049                }
1050            }
1051
1052            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
1053            if (c != null) {
1054                c.close();
1055            }
1056        } catch (RemoteException e) {
1057            // We will soon be killed...
1058        }
1059    }
1060
1061    private void postscan(String[] directories) throws RemoteException {
1062        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1063
1064        while (iterator.hasNext()) {
1065            FileCacheEntry entry = iterator.next();
1066            String path = entry.mPath;
1067
1068            // remove database entries for files that no longer exist.
1069            boolean fileMissing = false;
1070
1071            if (!entry.mSeenInFileSystem && !MtpConstants.isAbstractObject(entry.mFormat)) {
1072                if (inScanDirectory(path, directories)) {
1073                    // we didn't see this file in the scan directory.
1074                    fileMissing = true;
1075                } else {
1076                    // the file actually a directory or other abstract object
1077                    // or is outside of our scan directory,
1078                    // so we need to check for file existence here.
1079                    File testFile = new File(path);
1080                    if (!testFile.exists()) {
1081                        fileMissing = true;
1082                    }
1083                }
1084            }
1085
1086            if (fileMissing) {
1087                // do not delete missing playlists, since they may have been modified by the user.
1088                // the user can delete them in the media player instead.
1089                // instead, clear the path and lastModified fields in the row
1090                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1091                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1092
1093                if (MediaFile.isPlayListFileType(fileType)) {
1094                    ContentValues values = new ContentValues();
1095                    values.put(MediaStore.Audio.Playlists.DATA, "");
1096                    values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0);
1097                    mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId),
1098                            values, null, null);
1099                } else {
1100                    mMediaProvider.delete(ContentUris.withAppendedId(mFilesUri, entry.mRowId),
1101                            null, null);
1102                    iterator.remove();
1103                }
1104            }
1105        }
1106
1107        // handle playlists last, after we know what media files are on the storage.
1108        if (mProcessPlaylists) {
1109            processPlayLists();
1110        }
1111
1112        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1113            pruneDeadThumbnailFiles();
1114
1115        // allow GC to clean up
1116        mGenreCache = null;
1117        mPlayLists = null;
1118        mFileCache = null;
1119        mMediaProvider = null;
1120    }
1121
1122    private void initialize(String volumeName) {
1123        mMediaProvider = mContext.getContentResolver().acquireProvider("media");
1124
1125        mAudioUri = Audio.Media.getContentUri(volumeName);
1126        mVideoUri = Video.Media.getContentUri(volumeName);
1127        mImagesUri = Images.Media.getContentUri(volumeName);
1128        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
1129        mFilesUri = Files.getContentUri(volumeName);
1130
1131        if (!volumeName.equals("internal")) {
1132            // we only support playlists on external media
1133            mProcessPlaylists = true;
1134            mProcessGenres = true;
1135            mGenreCache = new HashMap<String, Uri>();
1136            mGenresUri = Genres.getContentUri(volumeName);
1137            mPlaylistsUri = Playlists.getContentUri(volumeName);
1138
1139            mCaseInsensitivePaths = !mContext.getResources().getBoolean(
1140                com.android.internal.R.bool.config_caseSensitiveExternalStorage);
1141            if (!Process.supportsProcesses()) {
1142                // Simulator uses host file system, so it should be case sensitive.
1143                mCaseInsensitivePaths = false;
1144            }
1145        }
1146    }
1147
1148    public void scanDirectories(String[] directories, String volumeName) {
1149        try {
1150            long start = System.currentTimeMillis();
1151            initialize(volumeName);
1152            prescan(null, true);
1153            long prescan = System.currentTimeMillis();
1154
1155            for (int i = 0; i < directories.length; i++) {
1156                processDirectory(directories[i], mClient);
1157            }
1158            long scan = System.currentTimeMillis();
1159            postscan(directories);
1160            long end = System.currentTimeMillis();
1161
1162            if (Config.LOGD) {
1163                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1164                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1165                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1166                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1167            }
1168        } catch (SQLException e) {
1169            // this might happen if the SD card is removed while the media scanner is running
1170            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1171        } catch (UnsupportedOperationException e) {
1172            // this might happen if the SD card is removed while the media scanner is running
1173            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1174        } catch (RemoteException e) {
1175            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1176        }
1177    }
1178
1179    // this function is used to scan a single file
1180    public Uri scanSingleFile(String path, String volumeName, String mimeType) {
1181        try {
1182            initialize(volumeName);
1183            prescan(path, true);
1184
1185            File file = new File(path);
1186
1187            // lastModified is in milliseconds on Files.
1188            long lastModifiedSeconds = file.lastModified() / 1000;
1189
1190            // always scan the file, so we can return the content://media Uri for existing files
1191            return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),false, true);
1192        } catch (RemoteException e) {
1193            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1194            return null;
1195        }
1196    }
1197
1198    public void scanMtpFile(String path, String volumeName, int objectHandle, int format) {
1199        MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1200        int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1201
1202        if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) &&
1203            !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType)) {
1204            // nothing to do
1205            return;
1206        }
1207
1208        mMtpObjectHandle = objectHandle;
1209        initialize(volumeName);
1210        try {
1211            if (MediaFile.isPlayListFileType(fileType)) {
1212                // build file cache so we can look up tracks in the playlist
1213                prescan(null, true);
1214
1215                String key = path;
1216                if (mMediaStoragePath != null && key.startsWith(mMediaStoragePath)) {
1217                    // MediaProvider uses external variant of path for _data, so we need to match
1218                    // against that path instead.
1219                    key = mExternalStoragePath + key.substring(mMediaStoragePath.length());
1220                }
1221                if (mCaseInsensitivePaths) {
1222                    key = path.toLowerCase();
1223                }
1224                FileCacheEntry entry = mFileCache.get(key);
1225                if (entry != null) {
1226                    processPlayList(entry);
1227                }
1228            } else {
1229                // MTP will create a file entry for us so we don't want to do it in prescan
1230                prescan(path, false);
1231
1232                File file = new File(path);
1233
1234                // lastModified is in milliseconds on Files.
1235                long lastModifiedSeconds = file.lastModified() / 1000;
1236
1237                // always scan the file, so we can return the content://media Uri for existing files
1238                mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(),
1239                    (format == MtpConstants.FORMAT_ASSOCIATION), true);
1240            }
1241        } catch (RemoteException e) {
1242            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1243        } finally {
1244            mMtpObjectHandle = 0;
1245        }
1246    }
1247
1248    // returns the number of matching file/directory names, starting from the right
1249    private int matchPaths(String path1, String path2) {
1250        int result = 0;
1251        int end1 = path1.length();
1252        int end2 = path2.length();
1253
1254        while (end1 > 0 && end2 > 0) {
1255            int slash1 = path1.lastIndexOf('/', end1 - 1);
1256            int slash2 = path2.lastIndexOf('/', end2 - 1);
1257            int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1258            int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1259            int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1260            int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1261            if (start1 < 0) start1 = 0; else start1++;
1262            if (start2 < 0) start2 = 0; else start2++;
1263            int length = end1 - start1;
1264            if (end2 - start2 != length) break;
1265            if (path1.regionMatches(true, start1, path2, start2, length)) {
1266                result++;
1267                end1 = start1 - 1;
1268                end2 = start2 - 1;
1269            } else break;
1270        }
1271
1272        return result;
1273    }
1274
1275    private boolean addPlayListEntry(String entry, String playListDirectory,
1276            Uri uri, ContentValues values, int index) {
1277
1278        // watch for trailing whitespace
1279        int entryLength = entry.length();
1280        while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--;
1281        // path should be longer than 3 characters.
1282        // avoid index out of bounds errors below by returning here.
1283        if (entryLength < 3) return false;
1284        if (entryLength < entry.length()) entry = entry.substring(0, entryLength);
1285
1286        // does entry appear to be an absolute path?
1287        // look for Unix or DOS absolute paths
1288        char ch1 = entry.charAt(0);
1289        boolean fullPath = (ch1 == '/' ||
1290                (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\'));
1291        // if we have a relative path, combine entry with playListDirectory
1292        if (!fullPath)
1293            entry = playListDirectory + entry;
1294
1295        //FIXME - should we look for "../" within the path?
1296
1297        // best matching MediaFile for the play list entry
1298        FileCacheEntry bestMatch = null;
1299
1300        // number of rightmost file/directory names for bestMatch
1301        int bestMatchLength = 0;
1302
1303        Iterator<FileCacheEntry> iterator = mFileCache.values().iterator();
1304        while (iterator.hasNext()) {
1305            FileCacheEntry cacheEntry = iterator.next();
1306            String path = cacheEntry.mPath;
1307
1308            if (path.equalsIgnoreCase(entry)) {
1309                bestMatch = cacheEntry;
1310                break;    // don't bother continuing search
1311            }
1312
1313            int matchLength = matchPaths(path, entry);
1314            if (matchLength > bestMatchLength) {
1315                bestMatch = cacheEntry;
1316                bestMatchLength = matchLength;
1317            }
1318        }
1319
1320        if (bestMatch == null) {
1321            return false;
1322        }
1323
1324        try {
1325        // OK, now we need to add this to the database
1326            values.clear();
1327            values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1328            values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId));
1329            mMediaProvider.insert(uri, values);
1330        } catch (RemoteException e) {
1331            Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e);
1332            return false;
1333        }
1334
1335        return true;
1336    }
1337
1338    private void processM3uPlayList(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.length() > 0 && line.charAt(0) != '#') {
1350                        values.clear();
1351                        if (addPlayListEntry(line, playListDirectory, uri, values, index))
1352                            index++;
1353                    }
1354                    line = reader.readLine();
1355                }
1356            }
1357        } catch (IOException e) {
1358            Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1359        } finally {
1360            try {
1361                if (reader != null)
1362                    reader.close();
1363            } catch (IOException e) {
1364                Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1365            }
1366        }
1367    }
1368
1369    private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) {
1370        BufferedReader reader = null;
1371        try {
1372            File f = new File(path);
1373            if (f.exists()) {
1374                reader = new BufferedReader(
1375                        new InputStreamReader(new FileInputStream(f)), 8192);
1376                String line = reader.readLine();
1377                int index = 0;
1378                while (line != null) {
1379                    // ignore comment lines, which begin with '#'
1380                    if (line.startsWith("File")) {
1381                        int equals = line.indexOf('=');
1382                        if (equals > 0) {
1383                            values.clear();
1384                            if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index))
1385                                index++;
1386                        }
1387                    }
1388                    line = reader.readLine();
1389                }
1390            }
1391        } catch (IOException e) {
1392            Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1393        } finally {
1394            try {
1395                if (reader != null)
1396                    reader.close();
1397            } catch (IOException e) {
1398                Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1399            }
1400        }
1401    }
1402
1403    class WplHandler implements ElementListener {
1404
1405        final ContentHandler handler;
1406        String playListDirectory;
1407        Uri uri;
1408        ContentValues values = new ContentValues();
1409        int index = 0;
1410
1411        public WplHandler(String playListDirectory, Uri uri) {
1412            this.playListDirectory = playListDirectory;
1413            this.uri = uri;
1414
1415            RootElement root = new RootElement("smil");
1416            Element body = root.getChild("body");
1417            Element seq = body.getChild("seq");
1418            Element media = seq.getChild("media");
1419            media.setElementListener(this);
1420
1421            this.handler = root.getContentHandler();
1422        }
1423
1424        public void start(Attributes attributes) {
1425            String path = attributes.getValue("", "src");
1426            if (path != null) {
1427                values.clear();
1428                if (addPlayListEntry(path, playListDirectory, uri, values, index)) {
1429                    index++;
1430                }
1431            }
1432        }
1433
1434       public void end() {
1435       }
1436
1437        ContentHandler getContentHandler() {
1438            return handler;
1439        }
1440    }
1441
1442    private void processWplPlayList(String path, String playListDirectory, Uri uri) {
1443        FileInputStream fis = null;
1444        try {
1445            File f = new File(path);
1446            if (f.exists()) {
1447                fis = new FileInputStream(f);
1448
1449                Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler());
1450            }
1451        } catch (SAXException e) {
1452            e.printStackTrace();
1453        } catch (IOException e) {
1454            e.printStackTrace();
1455        } finally {
1456            try {
1457                if (fis != null)
1458                    fis.close();
1459            } catch (IOException e) {
1460                Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1461            }
1462        }
1463    }
1464
1465    private void processPlayList(FileCacheEntry entry) throws RemoteException {
1466        String path = entry.mPath;
1467        ContentValues values = new ContentValues();
1468        int lastSlash = path.lastIndexOf('/');
1469        if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1470        Uri uri, membersUri;
1471        long rowId = entry.mRowId;
1472        if (rowId == 0) {
1473            // Create a new playlist
1474
1475            int lastDot = path.lastIndexOf('.');
1476            String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot));
1477            values.put(MediaStore.Audio.Playlists.NAME, name);
1478            values.put(MediaStore.Audio.Playlists.DATA, path);
1479            values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1480            uri = mMediaProvider.insert(mPlaylistsUri, values);
1481            rowId = ContentUris.parseId(uri);
1482            membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1483        } else {
1484            uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1485
1486            // update lastModified value of existing playlist
1487            values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1488            mMediaProvider.update(uri, values, null, null);
1489
1490            // delete members of existing playlist
1491            membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1492            mMediaProvider.delete(membersUri, null, null);
1493        }
1494
1495        String playListDirectory = path.substring(0, lastSlash + 1);
1496        MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1497        int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1498
1499        if (fileType == MediaFile.FILE_TYPE_M3U) {
1500            processM3uPlayList(path, playListDirectory, membersUri, values);
1501        } else if (fileType == MediaFile.FILE_TYPE_PLS) {
1502            processPlsPlayList(path, playListDirectory, membersUri, values);
1503        } else if (fileType == MediaFile.FILE_TYPE_WPL) {
1504            processWplPlayList(path, playListDirectory, membersUri);
1505        }
1506    }
1507
1508    private void processPlayLists() throws RemoteException {
1509        Iterator<FileCacheEntry> iterator = mPlayLists.iterator();
1510        while (iterator.hasNext()) {
1511            FileCacheEntry entry = iterator.next();
1512            // only process playlist files if they are new or have been modified since the last scan
1513            if (entry.mLastModifiedChanged) {
1514                processPlayList(entry);
1515            }
1516        }
1517    }
1518
1519    private native void processDirectory(String path, MediaScannerClient client);
1520    private native void processFile(String path, String mimeType, MediaScannerClient client);
1521    public native void setLocale(String locale);
1522
1523    public native byte[] extractAlbumArt(FileDescriptor fd);
1524
1525    private static native final void native_init();
1526    private native final void native_setup();
1527    private native final void native_finalize();
1528    @Override
1529    protected void finalize() {
1530        mContext.getContentResolver().releaseProvider(mMediaProvider);
1531        native_finalize();
1532    }
1533}
1534