MediaScanner.java revision 60cfad80bdf61db436643927337c2fb30186e99d
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 android.content.ContentProviderClient;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.database.SQLException;
25import android.drm.DrmManagerClient;
26import android.graphics.BitmapFactory;
27import android.mtp.MtpConstants;
28import android.net.Uri;
29import android.os.Environment;
30import android.os.RemoteException;
31import android.os.SystemProperties;
32import android.provider.MediaStore;
33import android.provider.MediaStore.Audio;
34import android.provider.MediaStore.Audio.Playlists;
35import android.provider.MediaStore.Files;
36import android.provider.MediaStore.Files.FileColumns;
37import android.provider.MediaStore.Images;
38import android.provider.MediaStore.Video;
39import android.provider.Settings;
40import android.sax.Element;
41import android.sax.ElementListener;
42import android.sax.RootElement;
43import android.system.ErrnoException;
44import android.system.Os;
45import android.text.TextUtils;
46import android.util.Log;
47import android.util.Xml;
48
49import dalvik.system.CloseGuard;
50
51import org.xml.sax.Attributes;
52import org.xml.sax.ContentHandler;
53import org.xml.sax.SAXException;
54
55import java.io.BufferedReader;
56import java.io.File;
57import java.io.FileDescriptor;
58import java.io.FileInputStream;
59import java.io.IOException;
60import java.io.InputStreamReader;
61import java.util.ArrayList;
62import java.util.HashMap;
63import java.util.HashSet;
64import java.util.Iterator;
65import java.util.Locale;
66import java.util.concurrent.atomic.AtomicBoolean;
67
68/**
69 * Internal service helper that no-one should use directly.
70 *
71 * The way the scan currently works is:
72 * - The Java MediaScannerService creates a MediaScanner (this class), and calls
73 *   MediaScanner.scanDirectories on it.
74 * - scanDirectories() calls the native processDirectory() for each of the specified directories.
75 * - the processDirectory() JNI method wraps the provided mediascanner client in a native
76 *   'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner
77 *   object (which got created when the Java MediaScanner was created).
78 * - native MediaScanner.processDirectory() calls
79 *   doProcessDirectory(), which recurses over the folder, and calls
80 *   native MyMediaScannerClient.scanFile() for every file whose extension matches.
81 * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile,
82 *   which calls doScanFile, which after some setup calls back down to native code, calling
83 *   MediaScanner.processFile().
84 * - MediaScanner.processFile() calls one of several methods, depending on the type of the
85 *   file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA.
86 * - each of these methods gets metadata key/value pairs from the file, and repeatedly
87 *   calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java
88 *   counterparts in this file.
89 * - Java handleStringTag() gathers the key/value pairs that it's interested in.
90 * - once processFile returns and we're back in Java code in doScanFile(), it calls
91 *   Java MyMediaScannerClient.endFile(), which takes all the data that's been
92 *   gathered and inserts an entry in to the database.
93 *
94 * In summary:
95 * Java MediaScannerService calls
96 * Java MediaScanner scanDirectories, which calls
97 * Java MediaScanner processDirectory (native method), which calls
98 * native MediaScanner processDirectory, which calls
99 * native MyMediaScannerClient scanFile, which calls
100 * Java MyMediaScannerClient scanFile, which calls
101 * Java MediaScannerClient doScanFile, which calls
102 * Java MediaScanner processFile (native method), which calls
103 * native MediaScanner processFile, which calls
104 * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls
105 * native MyMediaScanner handleStringTag, which calls
106 * Java MyMediaScanner handleStringTag.
107 * Once MediaScanner processFile returns, an entry is inserted in to the database.
108 *
109 * The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner.
110 *
111 * {@hide}
112 */
113public class MediaScanner implements AutoCloseable {
114    static {
115        System.loadLibrary("media_jni");
116        native_init();
117    }
118
119    private final static String TAG = "MediaScanner";
120
121    private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
122            Files.FileColumns._ID, // 0
123            Files.FileColumns.DATA, // 1
124            Files.FileColumns.FORMAT, // 2
125            Files.FileColumns.DATE_MODIFIED, // 3
126    };
127
128    private static final String[] ID_PROJECTION = new String[] {
129            Files.FileColumns._ID,
130    };
131
132    private static final int FILES_PRESCAN_ID_COLUMN_INDEX = 0;
133    private static final int FILES_PRESCAN_PATH_COLUMN_INDEX = 1;
134    private static final int FILES_PRESCAN_FORMAT_COLUMN_INDEX = 2;
135    private static final int FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX = 3;
136
137    private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] {
138            Audio.Playlists.Members.PLAYLIST_ID, // 0
139     };
140
141    private static final int ID_PLAYLISTS_COLUMN_INDEX = 0;
142    private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1;
143    private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2;
144
145    private static final String RINGTONES_DIR = "/ringtones/";
146    private static final String NOTIFICATIONS_DIR = "/notifications/";
147    private static final String ALARMS_DIR = "/alarms/";
148    private static final String MUSIC_DIR = "/music/";
149    private static final String PODCAST_DIR = "/podcasts/";
150
151    private static final String[] ID3_GENRES = {
152        // ID3v1 Genres
153        "Blues",
154        "Classic Rock",
155        "Country",
156        "Dance",
157        "Disco",
158        "Funk",
159        "Grunge",
160        "Hip-Hop",
161        "Jazz",
162        "Metal",
163        "New Age",
164        "Oldies",
165        "Other",
166        "Pop",
167        "R&B",
168        "Rap",
169        "Reggae",
170        "Rock",
171        "Techno",
172        "Industrial",
173        "Alternative",
174        "Ska",
175        "Death Metal",
176        "Pranks",
177        "Soundtrack",
178        "Euro-Techno",
179        "Ambient",
180        "Trip-Hop",
181        "Vocal",
182        "Jazz+Funk",
183        "Fusion",
184        "Trance",
185        "Classical",
186        "Instrumental",
187        "Acid",
188        "House",
189        "Game",
190        "Sound Clip",
191        "Gospel",
192        "Noise",
193        "AlternRock",
194        "Bass",
195        "Soul",
196        "Punk",
197        "Space",
198        "Meditative",
199        "Instrumental Pop",
200        "Instrumental Rock",
201        "Ethnic",
202        "Gothic",
203        "Darkwave",
204        "Techno-Industrial",
205        "Electronic",
206        "Pop-Folk",
207        "Eurodance",
208        "Dream",
209        "Southern Rock",
210        "Comedy",
211        "Cult",
212        "Gangsta",
213        "Top 40",
214        "Christian Rap",
215        "Pop/Funk",
216        "Jungle",
217        "Native American",
218        "Cabaret",
219        "New Wave",
220        "Psychadelic",
221        "Rave",
222        "Showtunes",
223        "Trailer",
224        "Lo-Fi",
225        "Tribal",
226        "Acid Punk",
227        "Acid Jazz",
228        "Polka",
229        "Retro",
230        "Musical",
231        "Rock & Roll",
232        "Hard Rock",
233        // The following genres are Winamp extensions
234        "Folk",
235        "Folk-Rock",
236        "National Folk",
237        "Swing",
238        "Fast Fusion",
239        "Bebob",
240        "Latin",
241        "Revival",
242        "Celtic",
243        "Bluegrass",
244        "Avantgarde",
245        "Gothic Rock",
246        "Progressive Rock",
247        "Psychedelic Rock",
248        "Symphonic Rock",
249        "Slow Rock",
250        "Big Band",
251        "Chorus",
252        "Easy Listening",
253        "Acoustic",
254        "Humour",
255        "Speech",
256        "Chanson",
257        "Opera",
258        "Chamber Music",
259        "Sonata",
260        "Symphony",
261        "Booty Bass",
262        "Primus",
263        "Porn Groove",
264        "Satire",
265        "Slow Jam",
266        "Club",
267        "Tango",
268        "Samba",
269        "Folklore",
270        "Ballad",
271        "Power Ballad",
272        "Rhythmic Soul",
273        "Freestyle",
274        "Duet",
275        "Punk Rock",
276        "Drum Solo",
277        "A capella",
278        "Euro-House",
279        "Dance Hall",
280        // The following ones seem to be fairly widely supported as well
281        "Goa",
282        "Drum & Bass",
283        "Club-House",
284        "Hardcore",
285        "Terror",
286        "Indie",
287        "Britpop",
288        null,
289        "Polsk Punk",
290        "Beat",
291        "Christian Gangsta",
292        "Heavy Metal",
293        "Black Metal",
294        "Crossover",
295        "Contemporary Christian",
296        "Christian Rock",
297        "Merengue",
298        "Salsa",
299        "Thrash Metal",
300        "Anime",
301        "JPop",
302        "Synthpop",
303        // 148 and up don't seem to have been defined yet.
304    };
305
306    private long mNativeContext;
307    private final Context mContext;
308    private final String mPackageName;
309    private final String mVolumeName;
310    private final ContentProviderClient mMediaProvider;
311    private final Uri mAudioUri;
312    private final Uri mVideoUri;
313    private final Uri mImagesUri;
314    private final Uri mThumbsUri;
315    private final Uri mPlaylistsUri;
316    private final Uri mFilesUri;
317    private final Uri mFilesUriNoNotify;
318    private final boolean mProcessPlaylists;
319    private final boolean mProcessGenres;
320    private int mMtpObjectHandle;
321
322    private final AtomicBoolean mClosed = new AtomicBoolean();
323    private final CloseGuard mCloseGuard = CloseGuard.get();
324
325    /** whether to use bulk inserts or individual inserts for each item */
326    private static final boolean ENABLE_BULK_INSERTS = true;
327
328    // used when scanning the image database so we know whether we have to prune
329    // old thumbnail files
330    private int mOriginalCount;
331    /** Whether the database had any entries in it before the scan started */
332    private boolean mWasEmptyPriorToScan = false;
333    /** Whether the scanner has set a default sound for the ringer ringtone. */
334    private boolean mDefaultRingtoneSet;
335    /** Whether the scanner has set a default sound for the notification ringtone. */
336    private boolean mDefaultNotificationSet;
337    /** Whether the scanner has set a default sound for the alarm ringtone. */
338    private boolean mDefaultAlarmSet;
339    /** The filename for the default sound for the ringer ringtone. */
340    private String mDefaultRingtoneFilename;
341    /** The filename for the default sound for the notification ringtone. */
342    private String mDefaultNotificationFilename;
343    /** The filename for the default sound for the alarm ringtone. */
344    private String mDefaultAlarmAlertFilename;
345    /**
346     * The prefix for system properties that define the default sound for
347     * ringtones. Concatenate the name of the setting from Settings
348     * to get the full system property.
349     */
350    private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config.";
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 final ArrayList<PlaylistEntry> mPlaylistEntries = new ArrayList<>();
382    private final ArrayList<FileEntry> mPlayLists = new ArrayList<>();
383
384    private MediaInserter mMediaInserter;
385
386    private DrmManagerClient mDrmManagerClient = null;
387
388    public MediaScanner(Context c, String volumeName) {
389        native_setup();
390        mContext = c;
391        mPackageName = c.getPackageName();
392        mVolumeName = volumeName;
393
394        mBitmapOptions.inSampleSize = 1;
395        mBitmapOptions.inJustDecodeBounds = true;
396
397        setDefaultRingtoneFileNames();
398
399        mMediaProvider = mContext.getContentResolver()
400                .acquireContentProviderClient(MediaStore.AUTHORITY);
401
402        mAudioUri = Audio.Media.getContentUri(volumeName);
403        mVideoUri = Video.Media.getContentUri(volumeName);
404        mImagesUri = Images.Media.getContentUri(volumeName);
405        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
406        mFilesUri = Files.getContentUri(volumeName);
407        mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();
408
409        if (!volumeName.equals("internal")) {
410            // we only support playlists on external media
411            mProcessPlaylists = true;
412            mProcessGenres = true;
413            mPlaylistsUri = Playlists.getContentUri(volumeName);
414        } else {
415            mProcessPlaylists = false;
416            mProcessGenres = false;
417            mPlaylistsUri = null;
418        }
419
420        final Locale locale = mContext.getResources().getConfiguration().locale;
421        if (locale != null) {
422            String language = locale.getLanguage();
423            String country = locale.getCountry();
424            if (language != null) {
425                if (country != null) {
426                    setLocale(language + "_" + country);
427                } else {
428                    setLocale(language);
429                }
430            }
431        }
432
433        mCloseGuard.open("close");
434    }
435
436    private void setDefaultRingtoneFileNames() {
437        mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
438                + Settings.System.RINGTONE);
439        mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
440                + Settings.System.NOTIFICATION_SOUND);
441        mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX
442                + Settings.System.ALARM_ALERT);
443    }
444
445    private final MyMediaScannerClient mClient = new MyMediaScannerClient();
446
447    private boolean isDrmEnabled() {
448        String prop = SystemProperties.get("drm.service.enabled");
449        return prop != null && prop.equals("true");
450    }
451
452    private class MyMediaScannerClient implements MediaScannerClient {
453
454        private String mArtist;
455        private String mAlbumArtist;    // use this if mArtist is missing
456        private String mAlbum;
457        private String mTitle;
458        private String mComposer;
459        private String mGenre;
460        private String mMimeType;
461        private int mFileType;
462        private int mTrack;
463        private int mYear;
464        private int mDuration;
465        private String mPath;
466        private long mLastModified;
467        private long mFileSize;
468        private String mWriter;
469        private int mCompilation;
470        private boolean mIsDrm;
471        private boolean mNoMedia;   // flag to suppress file from appearing in media tables
472        private int mWidth;
473        private int mHeight;
474
475        public FileEntry beginFile(String path, String mimeType, long lastModified,
476                long fileSize, boolean isDirectory, boolean noMedia) {
477            mMimeType = mimeType;
478            mFileType = 0;
479            mFileSize = fileSize;
480            mIsDrm = false;
481
482            if (!isDirectory) {
483                if (!noMedia && isNoMediaFile(path)) {
484                    noMedia = true;
485                }
486                mNoMedia = noMedia;
487
488                // try mimeType first, if it is specified
489                if (mimeType != null) {
490                    mFileType = MediaFile.getFileTypeForMimeType(mimeType);
491                }
492
493                // if mimeType was not specified, compute file type based on file extension.
494                if (mFileType == 0) {
495                    MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
496                    if (mediaFileType != null) {
497                        mFileType = mediaFileType.fileType;
498                        if (mMimeType == null) {
499                            mMimeType = mediaFileType.mimeType;
500                        }
501                    }
502                }
503
504                if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) {
505                    mFileType = getFileTypeFromDrm(path);
506                }
507            }
508
509            FileEntry entry = makeEntryFor(path);
510            // add some slack to avoid a rounding error
511            long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
512            boolean wasModified = delta > 1 || delta < -1;
513            if (entry == null || wasModified) {
514                if (wasModified) {
515                    entry.mLastModified = lastModified;
516                } else {
517                    entry = new FileEntry(0, path, lastModified,
518                            (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
519                }
520                entry.mLastModifiedChanged = true;
521            }
522
523            if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) {
524                mPlayLists.add(entry);
525                // we don't process playlists in the main scan, so return null
526                return null;
527            }
528
529            // clear all the metadata
530            mArtist = null;
531            mAlbumArtist = null;
532            mAlbum = null;
533            mTitle = null;
534            mComposer = null;
535            mGenre = null;
536            mTrack = 0;
537            mYear = 0;
538            mDuration = 0;
539            mPath = path;
540            mLastModified = lastModified;
541            mWriter = null;
542            mCompilation = 0;
543            mWidth = 0;
544            mHeight = 0;
545
546            return entry;
547        }
548
549        @Override
550        public void scanFile(String path, long lastModified, long fileSize,
551                boolean isDirectory, boolean noMedia) {
552            // This is the callback funtion from native codes.
553            // Log.v(TAG, "scanFile: "+path);
554            doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
555        }
556
557        public Uri doScanFile(String path, String mimeType, long lastModified,
558                long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
559            Uri result = null;
560//            long t1 = System.currentTimeMillis();
561            try {
562                FileEntry entry = beginFile(path, mimeType, lastModified,
563                        fileSize, isDirectory, noMedia);
564
565                // if this file was just inserted via mtp, set the rowid to zero
566                // (even though it already exists in the database), to trigger
567                // the correct code path for updating its entry
568                if (mMtpObjectHandle != 0) {
569                    entry.mRowId = 0;
570                }
571                // rescan for metadata if file was modified since last scan
572                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
573                    if (noMedia) {
574                        result = endFile(entry, false, false, false, false, false);
575                    } else {
576                        String lowpath = path.toLowerCase(Locale.ROOT);
577                        boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
578                        boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
579                        boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
580                        boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
581                        boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
582                            (!ringtones && !notifications && !alarms && !podcasts);
583
584                        boolean isaudio = MediaFile.isAudioFileType(mFileType);
585                        boolean isvideo = MediaFile.isVideoFileType(mFileType);
586                        boolean isimage = MediaFile.isImageFileType(mFileType);
587
588                        if (isaudio || isvideo || isimage) {
589                            path = Environment.maybeTranslateEmulatedPathToInternal(new File(path))
590                                    .getAbsolutePath();
591                        }
592
593                        // we only extract metadata for audio and video files
594                        if (isaudio || isvideo) {
595                            processFile(path, mimeType, this);
596                        }
597
598                        if (isimage) {
599                            processImageFile(path);
600                        }
601
602                        result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
603                    }
604                }
605            } catch (RemoteException e) {
606                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
607            }
608//            long t2 = System.currentTimeMillis();
609//            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
610            return result;
611        }
612
613        private int parseSubstring(String s, int start, int defaultValue) {
614            int length = s.length();
615            if (start == length) return defaultValue;
616
617            char ch = s.charAt(start++);
618            // return defaultValue if we have no integer at all
619            if (ch < '0' || ch > '9') return defaultValue;
620
621            int result = ch - '0';
622            while (start < length) {
623                ch = s.charAt(start++);
624                if (ch < '0' || ch > '9') return result;
625                result = result * 10 + (ch - '0');
626            }
627
628            return result;
629        }
630
631        public void handleStringTag(String name, String value) {
632            if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
633                // Don't trim() here, to preserve the special \001 character
634                // used to force sorting. The media provider will trim() before
635                // inserting the title in to the database.
636                mTitle = value;
637            } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
638                mArtist = value.trim();
639            } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
640                    || name.equalsIgnoreCase("band") || name.startsWith("band;")) {
641                mAlbumArtist = value.trim();
642            } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {
643                mAlbum = value.trim();
644            } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {
645                mComposer = value.trim();
646            } else if (mProcessGenres &&
647                    (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) {
648                mGenre = getGenreName(value);
649            } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {
650                mYear = parseSubstring(value, 0, 0);
651            } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {
652                // track number might be of the form "2/12"
653                // we just read the number before the slash
654                int num = parseSubstring(value, 0, 0);
655                mTrack = (mTrack / 1000) * 1000 + num;
656            } else if (name.equalsIgnoreCase("discnumber") ||
657                    name.equals("set") || name.startsWith("set;")) {
658                // set number might be of the form "1/3"
659                // we just read the number before the slash
660                int num = parseSubstring(value, 0, 0);
661                mTrack = (num * 1000) + (mTrack % 1000);
662            } else if (name.equalsIgnoreCase("duration")) {
663                mDuration = parseSubstring(value, 0, 0);
664            } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
665                mWriter = value.trim();
666            } else if (name.equalsIgnoreCase("compilation")) {
667                mCompilation = parseSubstring(value, 0, 0);
668            } else if (name.equalsIgnoreCase("isdrm")) {
669                mIsDrm = (parseSubstring(value, 0, 0) == 1);
670            } else if (name.equalsIgnoreCase("width")) {
671                mWidth = parseSubstring(value, 0, 0);
672            } else if (name.equalsIgnoreCase("height")) {
673                mHeight = parseSubstring(value, 0, 0);
674            } else {
675                //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")");
676            }
677        }
678
679        private boolean convertGenreCode(String input, String expected) {
680            String output = getGenreName(input);
681            if (output.equals(expected)) {
682                return true;
683            } else {
684                Log.d(TAG, "'" + input + "' -> '" + output + "', expected '" + expected + "'");
685                return false;
686            }
687        }
688        private void testGenreNameConverter() {
689            convertGenreCode("2", "Country");
690            convertGenreCode("(2)", "Country");
691            convertGenreCode("(2", "(2");
692            convertGenreCode("2 Foo", "Country");
693            convertGenreCode("(2) Foo", "Country");
694            convertGenreCode("(2 Foo", "(2 Foo");
695            convertGenreCode("2Foo", "2Foo");
696            convertGenreCode("(2)Foo", "Country");
697            convertGenreCode("200 Foo", "Foo");
698            convertGenreCode("(200) Foo", "Foo");
699            convertGenreCode("200Foo", "200Foo");
700            convertGenreCode("(200)Foo", "Foo");
701            convertGenreCode("200)Foo", "200)Foo");
702            convertGenreCode("200) Foo", "200) Foo");
703        }
704
705        public String getGenreName(String genreTagValue) {
706
707            if (genreTagValue == null) {
708                return null;
709            }
710            final int length = genreTagValue.length();
711
712            if (length > 0) {
713                boolean parenthesized = false;
714                StringBuffer number = new StringBuffer();
715                int i = 0;
716                for (; i < length; ++i) {
717                    char c = genreTagValue.charAt(i);
718                    if (i == 0 && c == '(') {
719                        parenthesized = true;
720                    } else if (Character.isDigit(c)) {
721                        number.append(c);
722                    } else {
723                        break;
724                    }
725                }
726                char charAfterNumber = i < length ? genreTagValue.charAt(i) : ' ';
727                if ((parenthesized && charAfterNumber == ')')
728                        || !parenthesized && Character.isWhitespace(charAfterNumber)) {
729                    try {
730                        short genreIndex = Short.parseShort(number.toString());
731                        if (genreIndex >= 0) {
732                            if (genreIndex < ID3_GENRES.length && ID3_GENRES[genreIndex] != null) {
733                                return ID3_GENRES[genreIndex];
734                            } else if (genreIndex == 0xFF) {
735                                return null;
736                            } else if (genreIndex < 0xFF && (i + 1) < length) {
737                                // genre is valid but unknown,
738                                // if there is a string after the value we take it
739                                if (parenthesized && charAfterNumber == ')') {
740                                    i++;
741                                }
742                                String ret = genreTagValue.substring(i).trim();
743                                if (ret.length() != 0) {
744                                    return ret;
745                                }
746                            } else {
747                                // else return the number, without parentheses
748                                return number.toString();
749                            }
750                        }
751                    } catch (NumberFormatException e) {
752                    }
753                }
754            }
755
756            return genreTagValue;
757        }
758
759        private void processImageFile(String path) {
760            try {
761                mBitmapOptions.outWidth = 0;
762                mBitmapOptions.outHeight = 0;
763                BitmapFactory.decodeFile(path, mBitmapOptions);
764                mWidth = mBitmapOptions.outWidth;
765                mHeight = mBitmapOptions.outHeight;
766            } catch (Throwable th) {
767                // ignore;
768            }
769        }
770
771        public void setMimeType(String mimeType) {
772            if ("audio/mp4".equals(mMimeType) &&
773                    mimeType.startsWith("video")) {
774                // for feature parity with Donut, we force m4a files to keep the
775                // audio/mp4 mimetype, even if they are really "enhanced podcasts"
776                // with a video track
777                return;
778            }
779            mMimeType = mimeType;
780            mFileType = MediaFile.getFileTypeForMimeType(mimeType);
781        }
782
783        /**
784         * Formats the data into a values array suitable for use with the Media
785         * Content Provider.
786         *
787         * @return a map of values
788         */
789        private ContentValues toValues() {
790            ContentValues map = new ContentValues();
791
792            map.put(MediaStore.MediaColumns.DATA, mPath);
793            map.put(MediaStore.MediaColumns.TITLE, mTitle);
794            map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified);
795            map.put(MediaStore.MediaColumns.SIZE, mFileSize);
796            map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
797            map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm);
798
799            String resolution = null;
800            if (mWidth > 0 && mHeight > 0) {
801                map.put(MediaStore.MediaColumns.WIDTH, mWidth);
802                map.put(MediaStore.MediaColumns.HEIGHT, mHeight);
803                resolution = mWidth + "x" + mHeight;
804            }
805
806            if (!mNoMedia) {
807                if (MediaFile.isVideoFileType(mFileType)) {
808                    map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0
809                            ? mArtist : MediaStore.UNKNOWN_STRING));
810                    map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0
811                            ? mAlbum : MediaStore.UNKNOWN_STRING));
812                    map.put(Video.Media.DURATION, mDuration);
813                    if (resolution != null) {
814                        map.put(Video.Media.RESOLUTION, resolution);
815                    }
816                } else if (MediaFile.isImageFileType(mFileType)) {
817                    // FIXME - add DESCRIPTION
818                } else if (MediaFile.isAudioFileType(mFileType)) {
819                    map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
820                            mArtist : MediaStore.UNKNOWN_STRING);
821                    map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
822                            mAlbumArtist.length() > 0) ? mAlbumArtist : null);
823                    map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
824                            mAlbum : MediaStore.UNKNOWN_STRING);
825                    map.put(Audio.Media.COMPOSER, mComposer);
826                    map.put(Audio.Media.GENRE, mGenre);
827                    if (mYear != 0) {
828                        map.put(Audio.Media.YEAR, mYear);
829                    }
830                    map.put(Audio.Media.TRACK, mTrack);
831                    map.put(Audio.Media.DURATION, mDuration);
832                    map.put(Audio.Media.COMPILATION, mCompilation);
833                }
834            }
835            return map;
836        }
837
838        private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
839                boolean alarms, boolean music, boolean podcasts)
840                throws RemoteException {
841            // update database
842
843            // use album artist if artist is missing
844            if (mArtist == null || mArtist.length() == 0) {
845                mArtist = mAlbumArtist;
846            }
847
848            ContentValues values = toValues();
849            String title = values.getAsString(MediaStore.MediaColumns.TITLE);
850            if (title == null || TextUtils.isEmpty(title.trim())) {
851                title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
852                values.put(MediaStore.MediaColumns.TITLE, title);
853            }
854            String album = values.getAsString(Audio.Media.ALBUM);
855            if (MediaStore.UNKNOWN_STRING.equals(album)) {
856                album = values.getAsString(MediaStore.MediaColumns.DATA);
857                // extract last path segment before file name
858                int lastSlash = album.lastIndexOf('/');
859                if (lastSlash >= 0) {
860                    int previousSlash = 0;
861                    while (true) {
862                        int idx = album.indexOf('/', previousSlash + 1);
863                        if (idx < 0 || idx >= lastSlash) {
864                            break;
865                        }
866                        previousSlash = idx;
867                    }
868                    if (previousSlash != 0) {
869                        album = album.substring(previousSlash + 1, lastSlash);
870                        values.put(Audio.Media.ALBUM, album);
871                    }
872                }
873            }
874            long rowId = entry.mRowId;
875            if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
876                // Only set these for new entries. For existing entries, they
877                // may have been modified later, and we want to keep the current
878                // values so that custom ringtones still show up in the ringtone
879                // picker.
880                values.put(Audio.Media.IS_RINGTONE, ringtones);
881                values.put(Audio.Media.IS_NOTIFICATION, notifications);
882                values.put(Audio.Media.IS_ALARM, alarms);
883                values.put(Audio.Media.IS_MUSIC, music);
884                values.put(Audio.Media.IS_PODCAST, podcasts);
885            } else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) {
886                ExifInterface exif = null;
887                try {
888                    exif = new ExifInterface(entry.mPath);
889                } catch (IOException ex) {
890                    // exif is null
891                }
892                if (exif != null) {
893                    float[] latlng = new float[2];
894                    if (exif.getLatLong(latlng)) {
895                        values.put(Images.Media.LATITUDE, latlng[0]);
896                        values.put(Images.Media.LONGITUDE, latlng[1]);
897                    }
898
899                    long time = exif.getGpsDateTime();
900                    if (time != -1) {
901                        values.put(Images.Media.DATE_TAKEN, time);
902                    } else {
903                        // If no time zone information is available, we should consider using
904                        // EXIF local time as taken time if the difference between file time
905                        // and EXIF local time is not less than 1 Day, otherwise MediaProvider
906                        // will use file time as taken time.
907                        time = exif.getDateTime();
908                        if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) {
909                            values.put(Images.Media.DATE_TAKEN, time);
910                        }
911                    }
912
913                    int orientation = exif.getAttributeInt(
914                        ExifInterface.TAG_ORIENTATION, -1);
915                    if (orientation != -1) {
916                        // We only recognize a subset of orientation tag values.
917                        int degree;
918                        switch(orientation) {
919                            case ExifInterface.ORIENTATION_ROTATE_90:
920                                degree = 90;
921                                break;
922                            case ExifInterface.ORIENTATION_ROTATE_180:
923                                degree = 180;
924                                break;
925                            case ExifInterface.ORIENTATION_ROTATE_270:
926                                degree = 270;
927                                break;
928                            default:
929                                degree = 0;
930                                break;
931                        }
932                        values.put(Images.Media.ORIENTATION, degree);
933                    }
934                }
935            }
936
937            Uri tableUri = mFilesUri;
938            MediaInserter inserter = mMediaInserter;
939            if (!mNoMedia) {
940                if (MediaFile.isVideoFileType(mFileType)) {
941                    tableUri = mVideoUri;
942                } else if (MediaFile.isImageFileType(mFileType)) {
943                    tableUri = mImagesUri;
944                } else if (MediaFile.isAudioFileType(mFileType)) {
945                    tableUri = mAudioUri;
946                }
947            }
948            Uri result = null;
949            boolean needToSetSettings = false;
950            if (rowId == 0) {
951                if (mMtpObjectHandle != 0) {
952                    values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle);
953                }
954                if (tableUri == mFilesUri) {
955                    int format = entry.mFormat;
956                    if (format == 0) {
957                        format = MediaFile.getFormatCode(entry.mPath, mMimeType);
958                    }
959                    values.put(Files.FileColumns.FORMAT, format);
960                }
961                // Setting a flag in order not to use bulk insert for the file related with
962                // notifications, ringtones, and alarms, because the rowId of the inserted file is
963                // needed.
964                if (mWasEmptyPriorToScan) {
965                    if (notifications && !mDefaultNotificationSet) {
966                        if (TextUtils.isEmpty(mDefaultNotificationFilename) ||
967                                doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) {
968                            needToSetSettings = true;
969                        }
970                    } else if (ringtones && !mDefaultRingtoneSet) {
971                        if (TextUtils.isEmpty(mDefaultRingtoneFilename) ||
972                                doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) {
973                            needToSetSettings = true;
974                        }
975                    } else if (alarms && !mDefaultAlarmSet) {
976                        if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) ||
977                                doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) {
978                            needToSetSettings = true;
979                        }
980                    }
981                }
982
983                // New file, insert it.
984                // Directories need to be inserted before the files they contain, so they
985                // get priority when bulk inserting.
986                // If the rowId of the inserted file is needed, it gets inserted immediately,
987                // bypassing the bulk inserter.
988                if (inserter == null || needToSetSettings) {
989                    if (inserter != null) {
990                        inserter.flushAll();
991                    }
992                    result = mMediaProvider.insert(tableUri, values);
993                } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
994                    inserter.insertwithPriority(tableUri, values);
995                } else {
996                    inserter.insert(tableUri, values);
997                }
998
999                if (result != null) {
1000                    rowId = ContentUris.parseId(result);
1001                    entry.mRowId = rowId;
1002                }
1003            } else {
1004                // updated file
1005                result = ContentUris.withAppendedId(tableUri, rowId);
1006                // path should never change, and we want to avoid replacing mixed cased paths
1007                // with squashed lower case paths
1008                values.remove(MediaStore.MediaColumns.DATA);
1009
1010                int mediaType = 0;
1011                if (!MediaScanner.isNoMediaPath(entry.mPath)) {
1012                    int fileType = MediaFile.getFileTypeForMimeType(mMimeType);
1013                    if (MediaFile.isAudioFileType(fileType)) {
1014                        mediaType = FileColumns.MEDIA_TYPE_AUDIO;
1015                    } else if (MediaFile.isVideoFileType(fileType)) {
1016                        mediaType = FileColumns.MEDIA_TYPE_VIDEO;
1017                    } else if (MediaFile.isImageFileType(fileType)) {
1018                        mediaType = FileColumns.MEDIA_TYPE_IMAGE;
1019                    } else if (MediaFile.isPlayListFileType(fileType)) {
1020                        mediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
1021                    }
1022                    values.put(FileColumns.MEDIA_TYPE, mediaType);
1023                }
1024                mMediaProvider.update(result, values, null, null);
1025            }
1026
1027            if(needToSetSettings) {
1028                if (notifications) {
1029                    setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId);
1030                    mDefaultNotificationSet = true;
1031                } else if (ringtones) {
1032                    setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId);
1033                    mDefaultRingtoneSet = true;
1034                } else if (alarms) {
1035                    setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId);
1036                    mDefaultAlarmSet = true;
1037                }
1038            }
1039
1040            return result;
1041        }
1042
1043        private boolean doesPathHaveFilename(String path, String filename) {
1044            int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1;
1045            int filenameLength = filename.length();
1046            return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) &&
1047                    pathFilenameStart + filenameLength == path.length();
1048        }
1049
1050        private void setSettingIfNotSet(String settingName, Uri uri, long rowId) {
1051
1052            String existingSettingValue = Settings.System.getString(mContext.getContentResolver(),
1053                    settingName);
1054
1055            if (TextUtils.isEmpty(existingSettingValue)) {
1056                // Set the setting to the given URI
1057                Settings.System.putString(mContext.getContentResolver(), settingName,
1058                        ContentUris.withAppendedId(uri, rowId).toString());
1059            }
1060        }
1061
1062        private int getFileTypeFromDrm(String path) {
1063            if (!isDrmEnabled()) {
1064                return 0;
1065            }
1066
1067            int resultFileType = 0;
1068
1069            if (mDrmManagerClient == null) {
1070                mDrmManagerClient = new DrmManagerClient(mContext);
1071            }
1072
1073            if (mDrmManagerClient.canHandle(path, null)) {
1074                mIsDrm = true;
1075                String drmMimetype = mDrmManagerClient.getOriginalMimeType(path);
1076                if (drmMimetype != null) {
1077                    mMimeType = drmMimetype;
1078                    resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype);
1079                }
1080            }
1081            return resultFileType;
1082        }
1083
1084    }; // end of anonymous MediaScannerClient instance
1085
1086    private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
1087        Cursor c = null;
1088        String where = null;
1089        String[] selectionArgs = null;
1090
1091        mPlayLists.clear();
1092
1093        if (filePath != null) {
1094            // query for only one file
1095            where = MediaStore.Files.FileColumns._ID + ">?" +
1096                " AND " + Files.FileColumns.DATA + "=?";
1097            selectionArgs = new String[] { "", filePath };
1098        } else {
1099            where = MediaStore.Files.FileColumns._ID + ">?";
1100            selectionArgs = new String[] { "" };
1101        }
1102
1103        // Tell the provider to not delete the file.
1104        // If the file is truly gone the delete is unnecessary, and we want to avoid
1105        // accidentally deleting files that are really there (this may happen if the
1106        // filesystem is mounted and unmounted while the scanner is running).
1107        Uri.Builder builder = mFilesUri.buildUpon();
1108        builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
1109        MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build());
1110
1111        // Build the list of files from the content provider
1112        try {
1113            if (prescanFiles) {
1114                // First read existing files from the files table.
1115                // Because we'll be deleting entries for missing files as we go,
1116                // we need to query the database in small batches, to avoid problems
1117                // with CursorWindow positioning.
1118                long lastId = Long.MIN_VALUE;
1119                Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
1120                mWasEmptyPriorToScan = true;
1121
1122                while (true) {
1123                    selectionArgs[0] = "" + lastId;
1124                    if (c != null) {
1125                        c.close();
1126                        c = null;
1127                    }
1128                    c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION,
1129                            where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
1130                    if (c == null) {
1131                        break;
1132                    }
1133
1134                    int num = c.getCount();
1135
1136                    if (num == 0) {
1137                        break;
1138                    }
1139                    mWasEmptyPriorToScan = false;
1140                    while (c.moveToNext()) {
1141                        long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
1142                        String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
1143                        int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
1144                        long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
1145                        lastId = rowId;
1146
1147                        // Only consider entries with absolute path names.
1148                        // This allows storing URIs in the database without the
1149                        // media scanner removing them.
1150                        if (path != null && path.startsWith("/")) {
1151                            boolean exists = false;
1152                            try {
1153                                exists = Os.access(path, android.system.OsConstants.F_OK);
1154                            } catch (ErrnoException e1) {
1155                            }
1156                            if (!exists && !MtpConstants.isAbstractObject(format)) {
1157                                // do not delete missing playlists, since they may have been
1158                                // modified by the user.
1159                                // The user can delete them in the media player instead.
1160                                // instead, clear the path and lastModified fields in the row
1161                                MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
1162                                int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
1163
1164                                if (!MediaFile.isPlayListFileType(fileType)) {
1165                                    deleter.delete(rowId);
1166                                    if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
1167                                        deleter.flush();
1168                                        String parent = new File(path).getParent();
1169                                        mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
1170                                    }
1171                                }
1172                            }
1173                        }
1174                    }
1175                }
1176            }
1177        }
1178        finally {
1179            if (c != null) {
1180                c.close();
1181            }
1182            deleter.flush();
1183        }
1184
1185        // compute original size of images
1186        mOriginalCount = 0;
1187        c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null);
1188        if (c != null) {
1189            mOriginalCount = c.getCount();
1190            c.close();
1191        }
1192    }
1193
1194    private boolean inScanDirectory(String path, String[] directories) {
1195        for (int i = 0; i < directories.length; i++) {
1196            String directory = directories[i];
1197            if (path.startsWith(directory)) {
1198                return true;
1199            }
1200        }
1201        return false;
1202    }
1203
1204    private void pruneDeadThumbnailFiles() {
1205        HashSet<String> existingFiles = new HashSet<String>();
1206        String directory = "/sdcard/DCIM/.thumbnails";
1207        String [] files = (new File(directory)).list();
1208        Cursor c = null;
1209        if (files == null)
1210            files = new String[0];
1211
1212        for (int i = 0; i < files.length; i++) {
1213            String fullPathString = directory + "/" + files[i];
1214            existingFiles.add(fullPathString);
1215        }
1216
1217        try {
1218            c = mMediaProvider.query(
1219                    mThumbsUri,
1220                    new String [] { "_data" },
1221                    null,
1222                    null,
1223                    null, null);
1224            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
1225            if (c != null && c.moveToFirst()) {
1226                do {
1227                    String fullPathString = c.getString(0);
1228                    existingFiles.remove(fullPathString);
1229                } while (c.moveToNext());
1230            }
1231
1232            for (String fileToDelete : existingFiles) {
1233                if (false)
1234                    Log.v(TAG, "fileToDelete is " + fileToDelete);
1235                try {
1236                    (new File(fileToDelete)).delete();
1237                } catch (SecurityException ex) {
1238                }
1239            }
1240
1241            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
1242        } catch (RemoteException e) {
1243            // We will soon be killed...
1244        } finally {
1245            if (c != null) {
1246                c.close();
1247            }
1248        }
1249    }
1250
1251    static class MediaBulkDeleter {
1252        StringBuilder whereClause = new StringBuilder();
1253        ArrayList<String> whereArgs = new ArrayList<String>(100);
1254        final ContentProviderClient mProvider;
1255        final Uri mBaseUri;
1256
1257        public MediaBulkDeleter(ContentProviderClient provider, Uri baseUri) {
1258            mProvider = provider;
1259            mBaseUri = baseUri;
1260        }
1261
1262        public void delete(long id) throws RemoteException {
1263            if (whereClause.length() != 0) {
1264                whereClause.append(",");
1265            }
1266            whereClause.append("?");
1267            whereArgs.add("" + id);
1268            if (whereArgs.size() > 100) {
1269                flush();
1270            }
1271        }
1272        public void flush() throws RemoteException {
1273            int size = whereArgs.size();
1274            if (size > 0) {
1275                String [] foo = new String [size];
1276                foo = whereArgs.toArray(foo);
1277                int numrows = mProvider.delete(mBaseUri,
1278                        MediaStore.MediaColumns._ID + " IN (" +
1279                        whereClause.toString() + ")", foo);
1280                //Log.i("@@@@@@@@@", "rows deleted: " + numrows);
1281                whereClause.setLength(0);
1282                whereArgs.clear();
1283            }
1284        }
1285    }
1286
1287    private void postscan(String[] directories) throws RemoteException {
1288
1289        // handle playlists last, after we know what media files are on the storage.
1290        if (mProcessPlaylists) {
1291            processPlayLists();
1292        }
1293
1294        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
1295            pruneDeadThumbnailFiles();
1296
1297        // allow GC to clean up
1298        mPlayLists.clear();
1299    }
1300
1301    private void releaseResources() {
1302        // release the DrmManagerClient resources
1303        if (mDrmManagerClient != null) {
1304            mDrmManagerClient.close();
1305            mDrmManagerClient = null;
1306        }
1307    }
1308
1309    public void scanDirectories(String[] directories) {
1310        try {
1311            long start = System.currentTimeMillis();
1312            prescan(null, true);
1313            long prescan = System.currentTimeMillis();
1314
1315            if (ENABLE_BULK_INSERTS) {
1316                // create MediaInserter for bulk inserts
1317                mMediaInserter = new MediaInserter(mMediaProvider, 500);
1318            }
1319
1320            for (int i = 0; i < directories.length; i++) {
1321                processDirectory(directories[i], mClient);
1322            }
1323
1324            if (ENABLE_BULK_INSERTS) {
1325                // flush remaining inserts
1326                mMediaInserter.flushAll();
1327                mMediaInserter = null;
1328            }
1329
1330            long scan = System.currentTimeMillis();
1331            postscan(directories);
1332            long end = System.currentTimeMillis();
1333
1334            if (false) {
1335                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
1336                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
1337                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
1338                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
1339            }
1340        } catch (SQLException e) {
1341            // this might happen if the SD card is removed while the media scanner is running
1342            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
1343        } catch (UnsupportedOperationException e) {
1344            // this might happen if the SD card is removed while the media scanner is running
1345            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
1346        } catch (RemoteException e) {
1347            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
1348        } finally {
1349            releaseResources();
1350        }
1351    }
1352
1353    // this function is used to scan a single file
1354    public Uri scanSingleFile(String path, String mimeType) {
1355        try {
1356            prescan(path, true);
1357
1358            File file = new File(path);
1359            if (!file.exists()) {
1360                return null;
1361            }
1362
1363            // lastModified is in milliseconds on Files.
1364            long lastModifiedSeconds = file.lastModified() / 1000;
1365
1366            // always scan the file, so we can return the content://media Uri for existing files
1367            return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
1368                    false, true, MediaScanner.isNoMediaPath(path));
1369        } catch (RemoteException e) {
1370            Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
1371            return null;
1372        } finally {
1373            releaseResources();
1374        }
1375    }
1376
1377    private static boolean isNoMediaFile(String path) {
1378        File file = new File(path);
1379        if (file.isDirectory()) return false;
1380
1381        // special case certain file names
1382        // I use regionMatches() instead of substring() below
1383        // to avoid memory allocation
1384        int lastSlash = path.lastIndexOf('/');
1385        if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
1386            // ignore those ._* files created by MacOS
1387            if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
1388                return true;
1389            }
1390
1391            // ignore album art files created by Windows Media Player:
1392            // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
1393            // and AlbumArt_{...}_Small.jpg
1394            if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
1395                if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
1396                        path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
1397                    return true;
1398                }
1399                int length = path.length() - lastSlash - 1;
1400                if ((length == 17 && path.regionMatches(
1401                        true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
1402                        (length == 10
1403                         && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
1404                    return true;
1405                }
1406            }
1407        }
1408        return false;
1409    }
1410
1411    private static HashMap<String,String> mNoMediaPaths = new HashMap<String,String>();
1412    private static HashMap<String,String> mMediaPaths = new HashMap<String,String>();
1413
1414    /* MediaProvider calls this when a .nomedia file is added or removed */
1415    public static void clearMediaPathCache(boolean clearMediaPaths, boolean clearNoMediaPaths) {
1416        synchronized (MediaScanner.class) {
1417            if (clearMediaPaths) {
1418                mMediaPaths.clear();
1419            }
1420            if (clearNoMediaPaths) {
1421                mNoMediaPaths.clear();
1422            }
1423        }
1424    }
1425
1426    public static boolean isNoMediaPath(String path) {
1427        if (path == null) {
1428            return false;
1429        }
1430        // return true if file or any parent directory has name starting with a dot
1431        if (path.indexOf("/.") >= 0) {
1432            return true;
1433        }
1434
1435        int firstSlash = path.lastIndexOf('/');
1436        if (firstSlash <= 0) {
1437            return false;
1438        }
1439        String parent = path.substring(0,  firstSlash);
1440
1441        synchronized (MediaScanner.class) {
1442            if (mNoMediaPaths.containsKey(parent)) {
1443                return true;
1444            } else if (!mMediaPaths.containsKey(parent)) {
1445                // check to see if any parent directories have a ".nomedia" file
1446                // start from 1 so we don't bother checking in the root directory
1447                int offset = 1;
1448                while (offset >= 0) {
1449                    int slashIndex = path.indexOf('/', offset);
1450                    if (slashIndex > offset) {
1451                        slashIndex++; // move past slash
1452                        File file = new File(path.substring(0, slashIndex) + ".nomedia");
1453                        if (file.exists()) {
1454                            // we have a .nomedia in one of the parent directories
1455                            mNoMediaPaths.put(parent, "");
1456                            return true;
1457                        }
1458                    }
1459                    offset = slashIndex;
1460                }
1461                mMediaPaths.put(parent, "");
1462            }
1463        }
1464
1465        return isNoMediaFile(path);
1466    }
1467
1468    public void scanMtpFile(String path, int objectHandle, int format) {
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(Files.getMtpObjectsUri(mVolumeName), 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(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(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(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(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(uri, values, null, null);
1815
1816            // delete members of existing playlist
1817            membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY);
1818            mMediaProvider.delete(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(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    private 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    @Override
1868    public void close() {
1869        mCloseGuard.close();
1870        if (mClosed.compareAndSet(false, true)) {
1871            mMediaProvider.close();
1872            native_finalize();
1873        }
1874    }
1875
1876    @Override
1877    protected void finalize() throws Throwable {
1878        try {
1879            mCloseGuard.warnIfOpen();
1880            close();
1881        } finally {
1882            super.finalize();
1883        }
1884    }
1885}
1886