MediaScanner.java revision 66dfcd465c02d78cb4ff22e7b509ee42d4309286
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 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();
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 = Libcore.os.access(path, libcore.io.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        if (files == null)
1189            files = new String[0];
1190
1191        for (int i = 0; i < files.length; i++) {
1192            String fullPathString = directory + "/" + files[i];
1193            existingFiles.add(fullPathString);
1194        }
1195
1196        try {
1197            Cursor c = mMediaProvider.query(
1198                    mPackageName,
1199                    mThumbsUri,
1200                    new String [] { "_data" },
1201                    null,
1202                    null,
1203                    null, null);
1204            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
1205            if (c != null && c.moveToFirst()) {
1206                do {
1207                    String fullPathString = c.getString(0);
1208                    existingFiles.remove(fullPathString);
1209                } while (c.moveToNext());
1210            }
1211
1212            for (String fileToDelete : existingFiles) {
1213                if (false)
1214                    Log.v(TAG, "fileToDelete is " + fileToDelete);
1215                try {
1216                    (new File(fileToDelete)).delete();
1217                } catch (SecurityException ex) {
1218                }
1219            }
1220
1221            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
1222            if (c != null) {
1223                c.close();
1224            }
1225        } catch (RemoteException e) {
1226            // We will soon be killed...
1227        }
1228    }
1229
1230    static class MediaBulkDeleter {
1231        StringBuilder whereClause = new StringBuilder();
1232        ArrayList<String> whereArgs = new ArrayList<String>(100);
1233        final IContentProvider mProvider;
1234        final String mPackageName;
1235        final Uri mBaseUri;
1236
1237        public MediaBulkDeleter(IContentProvider provider, String packageName, Uri baseUri) {
1238            mProvider = provider;
1239            mPackageName = packageName;
1240            mBaseUri = baseUri;
1241        }
1242
1243        public void delete(long id) throws RemoteException {
1244            if (whereClause.length() != 0) {
1245                whereClause.append(",");
1246            }
1247            whereClause.append("?");
1248            whereArgs.add("" + id);
1249            if (whereArgs.size() > 100) {
1250                flush();
1251            }
1252        }
1253        public void flush() throws RemoteException {
1254            int size = whereArgs.size();
1255            if (size > 0) {
1256                String [] foo = new String [size];
1257                foo = whereArgs.toArray(foo);
1258                int numrows = mProvider.delete(mPackageName, mBaseUri,
1259                        MediaStore.MediaColumns._ID + " IN (" +
1260                        whereClause.toString() + ")", foo);
1261                //Log.i("@@@@@@@@@", "rows deleted: " + numrows);
1262                whereClause.setLength(0);
1263                whereArgs.clear();
1264            }
1265        }
1266    }
1267
1268    private void postscan(String[] directories) throws RemoteException {
1269
1270        // handle playlists last, after we know what media files are on the storage.
1271        if (mProcessPlaylists) {
1272            processPlayLists();
1273        }
1274
1275        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1276            pruneDeadThumbnailFiles();
1277
1278        // allow GC to clean up
1279        mPlayLists = null;
1280        mMediaProvider = null;
1281    }
1282
1283    private void initialize(String volumeName) {
1284        mMediaProvider = mContext.getContentResolver().acquireProvider("media");
1285
1286        mAudioUri = Audio.Media.getContentUri(volumeName);
1287        mVideoUri = Video.Media.getContentUri(volumeName);
1288        mImagesUri = Images.Media.getContentUri(volumeName);
1289        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
1290        mFilesUri = Files.getContentUri(volumeName);
1291        mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();
1292
1293        if (!volumeName.equals("internal")) {
1294            // we only support playlists on external media
1295            mProcessPlaylists = true;
1296            mProcessGenres = true;
1297            mPlaylistsUri = Playlists.getContentUri(volumeName);
1298
1299            mCaseInsensitivePaths = true;
1300        }
1301    }
1302
1303    public void scanDirectories(String[] directories, String volumeName) {
1304        try {
1305            long start = System.currentTimeMillis();
1306            initialize(volumeName);
1307            prescan(null, true);
1308            long prescan = System.currentTimeMillis();
1309
1310            if (ENABLE_BULK_INSERTS) {
1311                // create MediaInserter for bulk inserts
1312                mMediaInserter = new MediaInserter(mMediaProvider, mPackageName, 500);
1313            }
1314
1315            for (int i = 0; i < directories.length; i++) {
1316                processDirectory(directories[i], mClient);
1317            }
1318
1319            if (ENABLE_BULK_INSERTS) {
1320                // flush remaining inserts
1321                mMediaInserter.flushAll();
1322                mMediaInserter = null;
1323            }
1324
1325            long scan = System.currentTimeMillis();
1326            postscan(directories);
1327            long end = System.currentTimeMillis();
1328
1329            if (false) {
1330                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1331                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1332                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1333                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1334            }
1335        } catch (SQLException e) {
1336            // this might happen if the SD card is removed while the media scanner is running
1337            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1338        } catch (UnsupportedOperationException e) {
1339            // this might happen if the SD card is removed while the media scanner is running
1340            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1341        } catch (RemoteException e) {
1342            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1343        }
1344    }
1345
1346    // this function is used to scan a single file
1347    public Uri scanSingleFile(String path, String volumeName, String mimeType) {
1348        try {
1349            initialize(volumeName);
1350            prescan(path, true);
1351
1352            File file = new File(path);
1353            if (!file.exists()) {
1354                return null;
1355            }
1356
1357            // lastModified is in milliseconds on Files.
1358            long lastModifiedSeconds = file.lastModified() / 1000;
1359
1360            // always scan the file, so we can return the content://media Uri for existing files
1361            return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
1362                    false, true, MediaScanner.isNoMediaPath(path));
1363        } catch (RemoteException e) {
1364            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1365            return null;
1366        }
1367    }
1368
1369    private static boolean isNoMediaFile(String path) {
1370        File file = new File(path);
1371        if (file.isDirectory()) return false;
1372
1373        // special case certain file names
1374        // I use regionMatches() instead of substring() below
1375        // to avoid memory allocation
1376        int lastSlash = path.lastIndexOf('/');
1377        if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
1378            // ignore those ._* files created by MacOS
1379            if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
1380                return true;
1381            }
1382
1383            // ignore album art files created by Windows Media Player:
1384            // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
1385            // and AlbumArt_{...}_Small.jpg
1386            if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
1387                if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
1388                        path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
1389                    return true;
1390                }
1391                int length = path.length() - lastSlash - 1;
1392                if ((length == 17 && path.regionMatches(
1393                        true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
1394                        (length == 10
1395                         && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
1396                    return true;
1397                }
1398            }
1399        }
1400        return false;
1401    }
1402
1403    public static boolean isNoMediaPath(String path) {
1404        if (path == null) return false;
1405
1406        // return true if file or any parent directory has name starting with a dot
1407        if (path.indexOf("/.") >= 0) return true;
1408
1409        // now check to see if any parent directories have a ".nomedia" file
1410        // start from 1 so we don't bother checking in the root directory
1411        int offset = 1;
1412        while (offset >= 0) {
1413            int slashIndex = path.indexOf('/', offset);
1414            if (slashIndex > offset) {
1415                slashIndex++; // move past slash
1416                File file = new File(path.substring(0, slashIndex) + ".nomedia");
1417                if (file.exists()) {
1418                    // we have a .nomedia in one of the parent directories
1419                    return true;
1420                }
1421            }
1422            offset = slashIndex;
1423        }
1424        return isNoMediaFile(path);
1425    }
1426
1427    public void scanMtpFile(String path, String volumeName, int objectHandle, int format) {
1428        initialize(volumeName);
1429        MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1430        int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1431        File file = new File(path);
1432        long lastModifiedSeconds = file.lastModified() / 1000;
1433
1434        if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) &&
1435            !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) &&
1436            !MediaFile.isDrmFileType(fileType)) {
1437
1438            // no need to use the media scanner, but we need to update last modified and file size
1439            ContentValues values = new ContentValues();
1440            values.put(Files.FileColumns.SIZE, file.length());
1441            values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds);
1442            try {
1443                String[] whereArgs = new String[] {  Integer.toString(objectHandle) };
1444                mMediaProvider.update(mPackageName, Files.getMtpObjectsUri(volumeName), values,
1445                        "_id=?", whereArgs);
1446            } catch (RemoteException e) {
1447                Log.e(TAG, "RemoteException in scanMtpFile", e);
1448            }
1449            return;
1450        }
1451
1452        mMtpObjectHandle = objectHandle;
1453        Cursor fileList = null;
1454        try {
1455            if (MediaFile.isPlayListFileType(fileType)) {
1456                // build file cache so we can look up tracks in the playlist
1457                prescan(null, true);
1458
1459                FileEntry entry = makeEntryFor(path);
1460                if (entry != null) {
1461                    fileList = mMediaProvider.query(mPackageName, mFilesUri,
1462                            FILES_PRESCAN_PROJECTION, null, null, null, null);
1463                    processPlayList(entry, fileList);
1464                }
1465            } else {
1466                // MTP will create a file entry for us so we don't want to do it in prescan
1467                prescan(path, false);
1468
1469                // always scan the file, so we can return the content://media Uri for existing files
1470                mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(),
1471                    (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path));
1472            }
1473        } catch (RemoteException e) {
1474            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1475        } finally {
1476            mMtpObjectHandle = 0;
1477            if (fileList != null) {
1478                fileList.close();
1479            }
1480        }
1481    }
1482
1483    FileEntry makeEntryFor(String path) {
1484        String where;
1485        String[] selectionArgs;
1486
1487        Cursor c = null;
1488        try {
1489            where = Files.FileColumns.DATA + "=?";
1490            selectionArgs = new String[] { path };
1491            c = mMediaProvider.query(mPackageName, mFilesUriNoNotify, FILES_PRESCAN_PROJECTION,
1492                    where, selectionArgs, null, null);
1493            if (c.moveToFirst()) {
1494                long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1495                int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
1496                long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
1497                return new FileEntry(rowId, path, lastModified, format);
1498            }
1499        } catch (RemoteException e) {
1500        } finally {
1501            if (c != null) {
1502                c.close();
1503            }
1504        }
1505        return null;
1506    }
1507
1508    // returns the number of matching file/directory names, starting from the right
1509    private int matchPaths(String path1, String path2) {
1510        int result = 0;
1511        int end1 = path1.length();
1512        int end2 = path2.length();
1513
1514        while (end1 > 0 && end2 > 0) {
1515            int slash1 = path1.lastIndexOf('/', end1 - 1);
1516            int slash2 = path2.lastIndexOf('/', end2 - 1);
1517            int backSlash1 = path1.lastIndexOf('\\', end1 - 1);
1518            int backSlash2 = path2.lastIndexOf('\\', end2 - 1);
1519            int start1 = (slash1 > backSlash1 ? slash1 : backSlash1);
1520            int start2 = (slash2 > backSlash2 ? slash2 : backSlash2);
1521            if (start1 < 0) start1 = 0; else start1++;
1522            if (start2 < 0) start2 = 0; else start2++;
1523            int length = end1 - start1;
1524            if (end2 - start2 != length) break;
1525            if (path1.regionMatches(true, start1, path2, start2, length)) {
1526                result++;
1527                end1 = start1 - 1;
1528                end2 = start2 - 1;
1529            } else break;
1530        }
1531
1532        return result;
1533    }
1534
1535    private boolean matchEntries(long rowId, String data) {
1536
1537        int len = mPlaylistEntries.size();
1538        boolean done = true;
1539        for (int i = 0; i < len; i++) {
1540            PlaylistEntry entry = mPlaylistEntries.get(i);
1541            if (entry.bestmatchlevel == Integer.MAX_VALUE) {
1542                continue; // this entry has been matched already
1543            }
1544            done = false;
1545            if (data.equalsIgnoreCase(entry.path)) {
1546                entry.bestmatchid = rowId;
1547                entry.bestmatchlevel = Integer.MAX_VALUE;
1548                continue; // no need for path matching
1549            }
1550
1551            int matchLength = matchPaths(data, entry.path);
1552            if (matchLength > entry.bestmatchlevel) {
1553                entry.bestmatchid = rowId;
1554                entry.bestmatchlevel = matchLength;
1555            }
1556        }
1557        return done;
1558    }
1559
1560    private void cachePlaylistEntry(String line, String playListDirectory) {
1561        PlaylistEntry entry = new PlaylistEntry();
1562        // watch for trailing whitespace
1563        int entryLength = line.length();
1564        while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--;
1565        // path should be longer than 3 characters.
1566        // avoid index out of bounds errors below by returning here.
1567        if (entryLength < 3) return;
1568        if (entryLength < line.length()) line = line.substring(0, entryLength);
1569
1570        // does entry appear to be an absolute path?
1571        // look for Unix or DOS absolute paths
1572        char ch1 = line.charAt(0);
1573        boolean fullPath = (ch1 == '/' ||
1574                (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\'));
1575        // if we have a relative path, combine entry with playListDirectory
1576        if (!fullPath)
1577            line = playListDirectory + line;
1578        entry.path = line;
1579        //FIXME - should we look for "../" within the path?
1580
1581        mPlaylistEntries.add(entry);
1582    }
1583
1584    private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) {
1585        fileList.moveToPosition(-1);
1586        while (fileList.moveToNext()) {
1587            long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1588            String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
1589            if (matchEntries(rowId, data)) {
1590                break;
1591            }
1592        }
1593
1594        int len = mPlaylistEntries.size();
1595        int index = 0;
1596        for (int i = 0; i < len; i++) {
1597            PlaylistEntry entry = mPlaylistEntries.get(i);
1598            if (entry.bestmatchlevel > 0) {
1599                try {
1600                    values.clear();
1601                    values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index));
1602                    values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid));
1603                    mMediaProvider.insert(mPackageName, playlistUri, values);
1604                    index++;
1605                } catch (RemoteException e) {
1606                    Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e);
1607                    return;
1608                }
1609            }
1610        }
1611        mPlaylistEntries.clear();
1612    }
1613
1614    private void processM3uPlayList(String path, String playListDirectory, Uri uri,
1615            ContentValues values, Cursor fileList) {
1616        BufferedReader reader = null;
1617        try {
1618            File f = new File(path);
1619            if (f.exists()) {
1620                reader = new BufferedReader(
1621                        new InputStreamReader(new FileInputStream(f)), 8192);
1622                String line = reader.readLine();
1623                mPlaylistEntries.clear();
1624                while (line != null) {
1625                    // ignore comment lines, which begin with '#'
1626                    if (line.length() > 0 && line.charAt(0) != '#') {
1627                        cachePlaylistEntry(line, playListDirectory);
1628                    }
1629                    line = reader.readLine();
1630                }
1631
1632                processCachedPlaylist(fileList, values, uri);
1633            }
1634        } catch (IOException e) {
1635            Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1636        } finally {
1637            try {
1638                if (reader != null)
1639                    reader.close();
1640            } catch (IOException e) {
1641                Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e);
1642            }
1643        }
1644    }
1645
1646    private void processPlsPlayList(String path, String playListDirectory, Uri uri,
1647            ContentValues values, Cursor fileList) {
1648        BufferedReader reader = null;
1649        try {
1650            File f = new File(path);
1651            if (f.exists()) {
1652                reader = new BufferedReader(
1653                        new InputStreamReader(new FileInputStream(f)), 8192);
1654                String line = reader.readLine();
1655                mPlaylistEntries.clear();
1656                while (line != null) {
1657                    // ignore comment lines, which begin with '#'
1658                    if (line.startsWith("File")) {
1659                        int equals = line.indexOf('=');
1660                        if (equals > 0) {
1661                            cachePlaylistEntry(line.substring(equals + 1), playListDirectory);
1662                        }
1663                    }
1664                    line = reader.readLine();
1665                }
1666
1667                processCachedPlaylist(fileList, values, uri);
1668            }
1669        } catch (IOException e) {
1670            Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1671        } finally {
1672            try {
1673                if (reader != null)
1674                    reader.close();
1675            } catch (IOException e) {
1676                Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e);
1677            }
1678        }
1679    }
1680
1681    class WplHandler implements ElementListener {
1682
1683        final ContentHandler handler;
1684        String playListDirectory;
1685
1686        public WplHandler(String playListDirectory, Uri uri, Cursor fileList) {
1687            this.playListDirectory = playListDirectory;
1688
1689            RootElement root = new RootElement("smil");
1690            Element body = root.getChild("body");
1691            Element seq = body.getChild("seq");
1692            Element media = seq.getChild("media");
1693            media.setElementListener(this);
1694
1695            this.handler = root.getContentHandler();
1696        }
1697
1698        @Override
1699        public void start(Attributes attributes) {
1700            String path = attributes.getValue("", "src");
1701            if (path != null) {
1702                cachePlaylistEntry(path, playListDirectory);
1703            }
1704        }
1705
1706       @Override
1707       public void end() {
1708       }
1709
1710        ContentHandler getContentHandler() {
1711            return handler;
1712        }
1713    }
1714
1715    private void processWplPlayList(String path, String playListDirectory, Uri uri,
1716            ContentValues values, Cursor fileList) {
1717        FileInputStream fis = null;
1718        try {
1719            File f = new File(path);
1720            if (f.exists()) {
1721                fis = new FileInputStream(f);
1722
1723                mPlaylistEntries.clear();
1724                Xml.parse(fis, Xml.findEncodingByName("UTF-8"),
1725                        new WplHandler(playListDirectory, uri, fileList).getContentHandler());
1726
1727                processCachedPlaylist(fileList, values, uri);
1728            }
1729        } catch (SAXException e) {
1730            e.printStackTrace();
1731        } catch (IOException e) {
1732            e.printStackTrace();
1733        } finally {
1734            try {
1735                if (fis != null)
1736                    fis.close();
1737            } catch (IOException e) {
1738                Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e);
1739            }
1740        }
1741    }
1742
1743    private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException {
1744        String path = entry.mPath;
1745        ContentValues values = new ContentValues();
1746        int lastSlash = path.lastIndexOf('/');
1747        if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path);
1748        Uri uri, membersUri;
1749        long rowId = entry.mRowId;
1750
1751        // make sure we have a name
1752        String name = values.getAsString(MediaStore.Audio.Playlists.NAME);
1753        if (name == null) {
1754            name = values.getAsString(MediaStore.MediaColumns.TITLE);
1755            if (name == null) {
1756                // extract name from file name
1757                int lastDot = path.lastIndexOf('.');
1758                name = (lastDot < 0 ? path.substring(lastSlash + 1)
1759                        : path.substring(lastSlash + 1, lastDot));
1760            }
1761        }
1762
1763        values.put(MediaStore.Audio.Playlists.NAME, name);
1764        values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified);
1765
1766        if (rowId == 0) {
1767            values.put(MediaStore.Audio.Playlists.DATA, path);
1768            uri = mMediaProvider.insert(mPackageName, mPlaylistsUri, values);
1769            rowId = ContentUris.parseId(uri);
1770            membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1771        } else {
1772            uri = ContentUris.withAppendedId(mPlaylistsUri, rowId);
1773            mMediaProvider.update(mPackageName, uri, values, null, null);
1774
1775            // delete members of existing playlist
1776            membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1777            mMediaProvider.delete(mPackageName, membersUri, null, null);
1778        }
1779
1780        String playListDirectory = path.substring(0, lastSlash + 1);
1781        MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1782        int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1783
1784        if (fileType == MediaFile.FILE_TYPE_M3U) {
1785            processM3uPlayList(path, playListDirectory, membersUri, values, fileList);
1786        } else if (fileType == MediaFile.FILE_TYPE_PLS) {
1787            processPlsPlayList(path, playListDirectory, membersUri, values, fileList);
1788        } else if (fileType == MediaFile.FILE_TYPE_WPL) {
1789            processWplPlayList(path, playListDirectory, membersUri, values, fileList);
1790        }
1791    }
1792
1793    private void processPlayLists() throws RemoteException {
1794        Iterator<FileEntry> iterator = mPlayLists.iterator();
1795        Cursor fileList = null;
1796        try {
1797            // use the files uri and projection because we need the format column,
1798            // but restrict the query to just audio files
1799            fileList = mMediaProvider.query(mPackageName, mFilesUri, FILES_PRESCAN_PROJECTION,
1800                    "media_type=2", null, null, null);
1801            while (iterator.hasNext()) {
1802                FileEntry entry = iterator.next();
1803                // only process playlist files if they are new or have been modified since the last scan
1804                if (entry.mLastModifiedChanged) {
1805                    processPlayList(entry, fileList);
1806                }
1807            }
1808        } catch (RemoteException e1) {
1809        } finally {
1810            if (fileList != null) {
1811                fileList.close();
1812            }
1813        }
1814    }
1815
1816    private native void processDirectory(String path, MediaScannerClient client);
1817    private native void processFile(String path, String mimeType, MediaScannerClient client);
1818    public native void setLocale(String locale);
1819
1820    public native byte[] extractAlbumArt(FileDescriptor fd);
1821
1822    private static native final void native_init();
1823    private native final void native_setup();
1824    private native final void native_finalize();
1825
1826    /**
1827     * Releases resources associated with this MediaScanner object.
1828     * It is considered good practice to call this method when
1829     * one is done using the MediaScanner object. After this method
1830     * is called, the MediaScanner object can no longer be used.
1831     */
1832    public void release() {
1833        native_finalize();
1834    }
1835
1836    @Override
1837    protected void finalize() {
1838        mContext.getContentResolver().releaseProvider(mMediaProvider);
1839        native_finalize();
1840    }
1841}
1842