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