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