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