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