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