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