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