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