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