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