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