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