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