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