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