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