• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 
17 package com.android.providers.media.scan;
18 
19 import static android.media.MediaMetadataRetriever.METADATA_KEY_ALBUM;
20 import static android.media.MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST;
21 import static android.media.MediaMetadataRetriever.METADATA_KEY_ARTIST;
22 import static android.media.MediaMetadataRetriever.METADATA_KEY_AUTHOR;
23 import static android.media.MediaMetadataRetriever.METADATA_KEY_BITRATE;
24 import static android.media.MediaMetadataRetriever.METADATA_KEY_BITS_PER_SAMPLE;
25 import static android.media.MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE;
26 import static android.media.MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER;
27 import static android.media.MediaMetadataRetriever.METADATA_KEY_COLOR_RANGE;
28 import static android.media.MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD;
29 import static android.media.MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER;
30 import static android.media.MediaMetadataRetriever.METADATA_KEY_COMPILATION;
31 import static android.media.MediaMetadataRetriever.METADATA_KEY_COMPOSER;
32 import static android.media.MediaMetadataRetriever.METADATA_KEY_DATE;
33 import static android.media.MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER;
34 import static android.media.MediaMetadataRetriever.METADATA_KEY_DURATION;
35 import static android.media.MediaMetadataRetriever.METADATA_KEY_GENRE;
36 import static android.media.MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT;
37 import static android.media.MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH;
38 import static android.media.MediaMetadataRetriever.METADATA_KEY_MIMETYPE;
39 import static android.media.MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS;
40 import static android.media.MediaMetadataRetriever.METADATA_KEY_SAMPLERATE;
41 import static android.media.MediaMetadataRetriever.METADATA_KEY_TITLE;
42 import static android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_CODEC_MIME_TYPE;
43 import static android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT;
44 import static android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION;
45 import static android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH;
46 import static android.media.MediaMetadataRetriever.METADATA_KEY_WRITER;
47 import static android.media.MediaMetadataRetriever.METADATA_KEY_YEAR;
48 import static android.provider.MediaStore.AUTHORITY;
49 import static android.provider.MediaStore.UNKNOWN_STRING;
50 import static android.provider.MediaStore.VOLUME_EXTERNAL;
51 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
52 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
53 
54 import static com.android.providers.media.flags.Flags.enableOemMetadata;
55 import static com.android.providers.media.flags.Flags.indexMediaLatitudeLongitude;
56 import static com.android.providers.media.util.FileUtils.canonicalize;
57 import static com.android.providers.media.util.IsoInterface.MAX_XMP_SIZE_BYTES;
58 import static com.android.providers.media.util.Metrics.translateReason;
59 
60 import static java.util.Objects.requireNonNull;
61 
62 import android.content.ComponentName;
63 import android.content.ContentProviderClient;
64 import android.content.ContentProviderOperation;
65 import android.content.ContentProviderResult;
66 import android.content.ContentResolver;
67 import android.content.ContentUris;
68 import android.content.ContentValues;
69 import android.content.Context;
70 import android.content.Intent;
71 import android.content.OperationApplicationException;
72 import android.content.ServiceConnection;
73 import android.content.pm.PackageManager;
74 import android.content.pm.ResolveInfo;
75 import android.database.Cursor;
76 import android.database.sqlite.SQLiteBlobTooBigException;
77 import android.database.sqlite.SQLiteDatabase;
78 import android.drm.DrmManagerClient;
79 import android.drm.DrmSupportInfo;
80 import android.graphics.BitmapFactory;
81 import android.media.ExifInterface;
82 import android.media.MediaMetadataRetriever;
83 import android.mtp.MtpConstants;
84 import android.net.Uri;
85 import android.os.Build;
86 import android.os.Bundle;
87 import android.os.CancellationSignal;
88 import android.os.Environment;
89 import android.os.IBinder;
90 import android.os.OperationCanceledException;
91 import android.os.ParcelFileDescriptor;
92 import android.os.RemoteException;
93 import android.os.SystemClock;
94 import android.os.SystemProperties;
95 import android.os.Trace;
96 import android.provider.IOemMetadataService;
97 import android.provider.MediaStore;
98 import android.provider.MediaStore.Audio.AudioColumns;
99 import android.provider.MediaStore.Audio.PlaylistsColumns;
100 import android.provider.MediaStore.Files.FileColumns;
101 import android.provider.MediaStore.Images.ImageColumns;
102 import android.provider.MediaStore.MediaColumns;
103 import android.provider.MediaStore.Video.VideoColumns;
104 import android.provider.OemMetadataService;
105 import android.provider.OemMetadataServiceWrapper;
106 import android.text.TextUtils;
107 import android.util.ArrayMap;
108 import android.util.ArraySet;
109 import android.util.Log;
110 import android.util.Pair;
111 
112 import androidx.annotation.GuardedBy;
113 import androidx.annotation.NonNull;
114 import androidx.annotation.Nullable;
115 import androidx.annotation.VisibleForTesting;
116 
117 import com.android.modules.utils.BackgroundThread;
118 import com.android.modules.utils.build.SdkLevel;
119 import com.android.providers.media.ConfigStore;
120 import com.android.providers.media.MediaProvider;
121 import com.android.providers.media.MediaVolume;
122 import com.android.providers.media.backupandrestore.RestoreExecutor;
123 import com.android.providers.media.flags.Flags;
124 import com.android.providers.media.util.DatabaseUtils;
125 import com.android.providers.media.util.ExifUtils;
126 import com.android.providers.media.util.FileUtils;
127 import com.android.providers.media.util.IsoInterface;
128 import com.android.providers.media.util.LongArray;
129 import com.android.providers.media.util.Metrics;
130 import com.android.providers.media.util.MimeUtils;
131 import com.android.providers.media.util.SpecialFormatDetector;
132 import com.android.providers.media.util.XmpDataParser;
133 import com.android.providers.media.util.XmpInterface;
134 
135 import java.io.File;
136 import java.io.FileInputStream;
137 import java.io.FileNotFoundException;
138 import java.io.IOException;
139 import java.nio.file.FileVisitResult;
140 import java.nio.file.FileVisitor;
141 import java.nio.file.Files;
142 import java.nio.file.Path;
143 import java.nio.file.Paths;
144 import java.nio.file.attribute.BasicFileAttributes;
145 import java.text.ParseException;
146 import java.text.SimpleDateFormat;
147 import java.util.ArrayList;
148 import java.util.Arrays;
149 import java.util.HashSet;
150 import java.util.Iterator;
151 import java.util.List;
152 import java.util.Locale;
153 import java.util.Map;
154 import java.util.Objects;
155 import java.util.Optional;
156 import java.util.Set;
157 import java.util.TimeZone;
158 import java.util.concurrent.CountDownLatch;
159 import java.util.concurrent.TimeUnit;
160 import java.util.concurrent.locks.Lock;
161 import java.util.concurrent.locks.ReentrantLock;
162 import java.util.regex.Matcher;
163 import java.util.regex.Pattern;
164 
165 /**
166  * Modern implementation of media scanner.
167  * <p>
168  * This is a bug-compatible reimplementation of the legacy media scanner, but
169  * written purely in managed code for better testability and long-term
170  * maintainability.
171  * <p>
172  * Initial tests shows it performing roughly on-par with the legacy scanner.
173  * <p>
174  * In general, we start by populating metadata based on file attributes, and
175  * then overwrite with any valid metadata found using
176  * {@link MediaMetadataRetriever}, {@link ExifInterface}, and
177  * {@link XmpInterface}, each with increasing levels of trust.
178  */
179 public class ModernMediaScanner implements MediaScanner {
180     private static final String TAG = "ModernMediaScanner";
181     private static final boolean LOGW = Log.isLoggable(TAG, Log.WARN);
182     private static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
183     private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
184 
185     // TODO: refactor to use UPSERT once we have SQLite 3.24.0
186 
187     // TODO: deprecate playlist editing
188     // TODO: deprecate PARENT column, since callers can't see directories
189 
190     @GuardedBy("S_DATE_FORMAT")
191     private static final SimpleDateFormat S_DATE_FORMAT;
192     @GuardedBy("S_DATE_FORMAT_WITH_MILLIS")
193     private static final SimpleDateFormat S_DATE_FORMAT_WITH_MILLIS;
194 
195     static {
196         S_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
197         S_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
198 
199         S_DATE_FORMAT_WITH_MILLIS = new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSS");
200         S_DATE_FORMAT_WITH_MILLIS.setTimeZone(TimeZone.getTimeZone("UTC"));
201     }
202 
203     private static final int BATCH_SIZE = 32;
204     // |excludeDirs * 2| < 1000 which is the max SQL expression size
205     // Because we add |excludeDir| and |excludeDir/| in the SQL expression to match dir and subdirs
206     // See SQLITE_MAX_EXPR_DEPTH in sqlite3.c
207     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
208     static final int MAX_EXCLUDE_DIRS = 450;
209 
210     private static final Pattern PATTERN_YEAR = Pattern.compile("([1-9][0-9][0-9][0-9])");
211 
212     private static final Pattern PATTERN_ALBUM_ART = Pattern.compile(
213             "(?i)(?:(?:^folder|(?:^AlbumArt(?:(?:_\\{.*\\}_)?(?:small|large))?))(?:\\.jpg$)|(?:\\._.*))");
214 
215     // The path of the MyFiles/Downloads directory shared from Chrome OS in ARC.
216     private static final Path ARC_MYFILES_DOWNLOADS_PATH = Paths.get(
217             "/storage/0000000000000000000000000000CAFEF00D2019/Downloads");
218 
219     // Check the same property as android.os.Build.IS_ARC.
220     private static final boolean IS_ARC =
221             SystemProperties.getBoolean("ro.boot.container", false);
222 
223     @NonNull
224     private final Context mContext;
225     private final DrmManagerClient mDrmClient;
226     private OemMetadataServiceWrapper mOemMetadataServiceWrapper;
227     @GuardedBy("mPendingCleanDirectories")
228     private final Set<String> mPendingCleanDirectories = new ArraySet<>();
229 
230     /**
231      * List of active scans.
232      */
233     @GuardedBy("mActiveScans")
234 
235     private final List<Scan> mActiveScans = new ArrayList<>();
236 
237     /**
238      * Holder that contains a reference count of the number of threads
239      * interested in a specific directory, along with a lock to ensure that
240      * parallel scans don't overlap and confuse each other.
241      */
242     private static class DirectoryLock {
243         public int count;
244         public final Lock lock = new ReentrantLock();
245     }
246 
247     /**
248      * Map from directory to locks designed to ensure that parallel scans don't
249      * overlap and confuse each other.
250      */
251     @GuardedBy("mDirectoryLocks")
252     private final Map<String, DirectoryLock> mDirectoryLocks = new ArrayMap<>();
253 
254     /**
255      * Set of MIME types that should be considered to be DRM, meaning we need to
256      * consult {@link DrmManagerClient} to obtain the actual MIME type.
257      */
258     private final Set<String> mDrmMimeTypes = new ArraySet<>();
259 
260     /**
261      * Set of MIME types that should be considered for fetching OEM metadata.
262      */
263     private Set<String> mOemSupportedMimeTypes;
264 
265     /**
266      * Default OemMetadataService implementation package.
267      */
268     private Optional<String> mDefaultOemMetadataServicePackage;
269 
270     /**
271      * Count down latch to process delay in connection to OemMetadataService.
272      */
273     private CountDownLatch mCountDownLatchForOemMetadataConnection = new CountDownLatch(1);
274 
ModernMediaScanner(@onNull Context context, @NonNull ConfigStore configStore)275     public ModernMediaScanner(@NonNull Context context, @NonNull ConfigStore configStore) {
276         mContext = requireNonNull(context);
277         mDrmClient = new DrmManagerClient(context);
278         mDefaultOemMetadataServicePackage = configStore.getDefaultOemMetadataServicePackage();
279 
280         // Dynamically collect the set of MIME types that should be considered
281         // to be DRM, as this can vary between devices
282         for (DrmSupportInfo info : mDrmClient.getAvailableDrmSupportInfo()) {
283             Iterator<String> mimeTypes = info.getMimeTypeIterator();
284             while (mimeTypes.hasNext()) {
285                 mDrmMimeTypes.add(mimeTypes.next());
286             }
287         }
288     }
289 
290     @Override
getOemSupportedMimeTypes()291     public Set<String> getOemSupportedMimeTypes() {
292         try {
293             // Return if no package implements OemMetadataService
294             if (!mDefaultOemMetadataServicePackage.isPresent()) {
295                 return new HashSet<>();
296             }
297 
298             // Setup connection if missing
299             if (mOemMetadataServiceWrapper == null) {
300                 connectOemMetadataServiceWrapper();
301             }
302 
303             // Return empty set if we cannot setup any connection
304             if (mOemMetadataServiceWrapper == null) {
305                 return new HashSet<>();
306             }
307 
308             return mOemMetadataServiceWrapper.getSupportedMimeTypes();
309         } catch (Exception e) {
310             Log.w(TAG, "Error in fetching OEM supported mimetypes", e);
311             return new HashSet<>();
312         }
313     }
314 
connectOemMetadataServiceWrapper()315     private synchronized void connectOemMetadataServiceWrapper() {
316         try {
317             if (!enableOemMetadata()) {
318                 return;
319             }
320 
321             // Return if wrapper is already initialised
322             if (mOemMetadataServiceWrapper != null) {
323                 return;
324             }
325 
326             if (!mDefaultOemMetadataServicePackage.isPresent()) {
327                 Log.v(TAG, "No default package listed for OEM Metadata service");
328                 return;
329             }
330 
331             Intent intent = new Intent(OemMetadataService.SERVICE_INTERFACE);
332             ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
333                     PackageManager.MATCH_ALL);
334             if (resolveInfo == null || resolveInfo.serviceInfo == null
335                     || resolveInfo.serviceInfo.packageName == null
336                     || !mDefaultOemMetadataServicePackage.get().equalsIgnoreCase(
337                     resolveInfo.serviceInfo.packageName)
338                     || resolveInfo.serviceInfo.permission == null
339                     || !resolveInfo.serviceInfo.permission.equalsIgnoreCase(
340                     OemMetadataService.BIND_OEM_METADATA_SERVICE_PERMISSION)) {
341                 Log.v(TAG, "No valid package found for OEM Metadata service");
342                 return;
343             }
344 
345             intent.setPackage(mDefaultOemMetadataServicePackage.get());
346             mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
347             mCountDownLatchForOemMetadataConnection.await(5, TimeUnit.SECONDS);
348         } catch (Exception e) {
349             Log.e(TAG, "Exception in connecting OemMetadataServiceWrapper", e);
350         }
351     }
352 
353     private ServiceConnection mServiceConnection = new ServiceConnection() {
354         @Override
355         public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
356             IOemMetadataService service = IOemMetadataService.Stub.asInterface(iBinder);
357             mOemMetadataServiceWrapper = new OemMetadataServiceWrapper(service);
358             mCountDownLatchForOemMetadataConnection.countDown();
359             Log.i(TAG, "Connected to OemMetadataService");
360         }
361 
362         @Override
363         public void onServiceDisconnected(ComponentName componentName) {
364             mOemMetadataServiceWrapper = null;
365             Log.w(TAG, "Disconnected from OemMetadataService");
366             mCountDownLatchForOemMetadataConnection = new CountDownLatch(1);
367         }
368 
369         @Override
370         public void onBindingDied(ComponentName name) {
371             Log.w(TAG, "Binding to OemMetadataService died");
372             mContext.unbindService(this);
373             mOemMetadataServiceWrapper = null;
374             mCountDownLatchForOemMetadataConnection = new CountDownLatch(1);
375         }
376     };
377 
378     @VisibleForTesting
getOemMetadataServiceConnection()379     public ServiceConnection getOemMetadataServiceConnection() {
380         return mServiceConnection;
381     }
382 
383     @Override
384     @NonNull
getContext()385     public Context getContext() {
386         return mContext;
387     }
388 
389     @Override
scanDirectory(@onNull File file, @ScanReason int reason)390     public void scanDirectory(@NonNull File file, @ScanReason int reason) {
391         requireNonNull(file);
392         try {
393             file = canonicalize(file);
394         } catch (IOException e) {
395             Log.e(TAG, "Couldn't canonicalize directory to scan" + file, e);
396             return;
397         }
398 
399         try (Scan scan = new Scan(file, reason)) {
400             scan.run();
401         } catch (FileNotFoundException e) {
402             Log.e(TAG, "Couldn't find directory to scan", e);
403         } catch (OperationCanceledException ignored) {
404             // No-op.
405         }
406     }
407 
408     @Override
409     @Nullable
scanFile(@onNull File file, @ScanReason int reason)410     public Uri scanFile(@NonNull File file, @ScanReason int reason) {
411         requireNonNull(file);
412         try {
413             file = canonicalize(file);
414         } catch (IOException e) {
415             Log.e(TAG, "Couldn't canonicalize file to scan" + file, e);
416             return null;
417         }
418 
419         try (Scan scan = new Scan(file, reason)) {
420             scan.run();
421             return scan.getFirstResult();
422         } catch (FileNotFoundException e) {
423             Log.e(TAG, "Couldn't find file to scan", e) ;
424             return null;
425         } catch (OperationCanceledException ignored) {
426             // No-op.
427             return null;
428         }
429     }
430 
431     @Override
onDetachVolume(@onNull MediaVolume volume)432     public void onDetachVolume(@NonNull MediaVolume volume) {
433         synchronized (mActiveScans) {
434             for (Scan scan : mActiveScans) {
435                 if (volume.equals(scan.mVolume)) {
436                     scan.mSignal.cancel();
437                 }
438             }
439         }
440     }
441 
442     @Override
onIdleScanStopped()443     public void onIdleScanStopped() {
444         synchronized (mActiveScans) {
445             for (Scan scan : mActiveScans) {
446                 if (scan.mReason == REASON_IDLE) {
447                     scan.mSignal.cancel();
448                 }
449             }
450         }
451     }
452 
453     /**
454      * Invalidate FUSE dentry cache while setting directory dirty
455      */
invalidateFuseDentryInBg(File file)456     private void invalidateFuseDentryInBg(File file) {
457         BackgroundThread.getExecutor().execute(() -> {
458             try (ContentProviderClient client =
459                          mContext.getContentResolver().acquireContentProviderClient(
460                                  MediaStore.AUTHORITY)) {
461                 ((MediaProvider) client.getLocalContentProvider()).invalidateFuseDentry(file);
462             }
463         });
464     }
465 
466 
467     @Override
onDirectoryDirty(@onNull File dir)468     public void onDirectoryDirty(@NonNull File dir) {
469         requireNonNull(dir);
470         try {
471             dir = canonicalize(dir);
472         } catch (IOException e) {
473             Log.e(TAG, "Couldn't canonicalize directory" + dir, e);
474             return;
475         }
476 
477         synchronized (mPendingCleanDirectories) {
478             mPendingCleanDirectories.remove(dir.getPath().toLowerCase(Locale.ROOT));
479             FileUtils.setDirectoryDirty(dir, /* isDirty */ true);
480             invalidateFuseDentryInBg(dir);
481         }
482     }
483 
addActiveScan(Scan scan)484     private void addActiveScan(Scan scan) {
485         synchronized (mActiveScans) {
486             mActiveScans.add(scan);
487         }
488     }
489 
removeActiveScan(Scan scan)490     private void removeActiveScan(Scan scan) {
491         synchronized (mActiveScans) {
492             mActiveScans.remove(scan);
493         }
494     }
495 
496     /**
497      * Individual scan request for a specific file or directory. When run it
498      * will traverse all included media files under the requested location,
499      * reconciling them against {@link MediaStore}.
500      */
501     private class Scan implements Runnable, FileVisitor<Path>, AutoCloseable {
502         private final ContentProviderClient mClient;
503         private final ContentResolver mResolver;
504 
505         private final File mRoot;
506         private final int mReason;
507         private final MediaVolume mVolume;
508         private final String mVolumeName;
509         private final Uri mFilesUri;
510         private final CancellationSignal mSignal;
511         private final Optional<RestoreExecutor> mRestoreExecutorOptional;
512         private final List<String> mExcludeDirs;
513 
514         private final long mStartGeneration;
515         private final boolean mSingleFile;
516         private final Set<String> mAcquiredDirectoryLocks = new ArraySet<>();
517         private final ArrayList<ContentProviderOperation> mPending = new ArrayList<>();
518         private final LongArray mScannedIds = new LongArray();
519         private final LongArray mUnknownIds = new LongArray();
520 
521         private long mFirstId = -1;
522 
523         private int mFileCount;
524         private int mInsertCount;
525         private int mUpdateCount;
526         private int mDeleteCount;
527 
528         /**
529          * Indicates if the nomedia directory tree is dirty. When a nomedia directory is dirty, we
530          * mark the top level nomedia as dirty. Hence if one of the sub directory in the nomedia
531          * directory is dirty, we consider the whole top level nomedia directory tree as dirty.
532          */
533         private boolean mIsDirectoryTreeDirty;
534 
535         /**
536          * Tracks hidden directory and hidden subdirectories in a directory tree.
537          */
538         private boolean mIsDirectoryTreeHidden = false;
539         private String mTopLevelHiddenDirectory;
540 
Scan(File root, int reason)541         Scan(File root, int reason) throws FileNotFoundException {
542             Trace.beginSection("Scanner.ctor");
543 
544             mClient = mContext.getContentResolver()
545                     .acquireContentProviderClient(MediaStore.AUTHORITY);
546             mResolver = ContentResolver.wrap(mClient.getLocalContentProvider());
547 
548             mRoot = root;
549             mReason = reason;
550 
551             if (FileUtils.contains(Environment.getStorageDirectory(), root)) {
552                 mVolume = MediaVolume.fromStorageVolume(FileUtils.getStorageVolume(mContext, root));
553             } else {
554                 mVolume = MediaVolume.fromInternal();
555             }
556             mVolumeName = mVolume.getName();
557             mFilesUri = MediaStore.Files.getContentUri(mVolumeName);
558             mSignal = new CancellationSignal();
559             mRestoreExecutorOptional = RestoreExecutor.getRestoreExecutor(mContext);
560 
561             mStartGeneration = MediaStore.getGeneration(mResolver, mVolumeName);
562             mSingleFile = mRoot.isFile();
563             mExcludeDirs = new ArrayList<>();
564 
565             Trace.endSection();
566         }
567 
568         @Override
run()569         public void run() {
570             addActiveScan(this);
571             try {
572                 runInternal();
573             } finally {
574                 removeActiveScan(this);
575             }
576         }
577 
runInternal()578         private void runInternal() {
579             final long startTime = SystemClock.elapsedRealtime();
580 
581             // First, scan everything that should be visible under requested
582             // location, tracking scanned IDs along the way
583             walkFileTree();
584 
585             // Second, reconcile all items known in the database against all the
586             // items we scanned above
587             if (mSingleFile && mScannedIds.size() == 1) {
588                 // We can safely skip this step if the scan targeted a single
589                 // file which we scanned above
590             } else {
591                 reconcileAndClean();
592             }
593 
594             // Third, resolve any playlists that we scanned
595             resolvePlaylists();
596 
597             if (!mSingleFile) {
598                 final long durationMillis = SystemClock.elapsedRealtime() - startTime;
599                 Metrics.logScan(mVolumeName, mReason, mFileCount, durationMillis,
600                         mInsertCount, mUpdateCount, mDeleteCount);
601             }
602         }
603 
walkFileTree()604         private void walkFileTree() {
605             mSignal.throwIfCanceled();
606 
607             File dirPath = mSingleFile ? mRoot.getParentFile() : mRoot;
608             final Pair<Boolean, Boolean> isDirScannableAndHidden = shouldScanPathAndIsPathHidden(
609                     dirPath);
610             if (isDirScannableAndHidden.first) {
611                 // This directory is scannable.
612                 Trace.beginSection("Scanner.walkFileTree");
613 
614                 if (isDirScannableAndHidden.second) {
615                     // This directory is hidden
616                     mIsDirectoryTreeHidden = true;
617                     mTopLevelHiddenDirectory = dirPath.getAbsolutePath();
618                 }
619 
620                 if (mSingleFile) {
621                     acquireDirectoryLock(mRoot.getParentFile().toPath().toString());
622                 }
623                 try {
624                     Files.walkFileTree(mRoot.toPath(), this);
625                     applyPending();
626                 } catch (IOException e) {
627                     // This should never happen, so yell loudly
628                     throw new IllegalStateException(e);
629                 } finally {
630                     if (mSingleFile) {
631                         releaseDirectoryLock(mRoot.getParentFile().toPath().toString());
632                     }
633                     Trace.endSection();
634                 }
635             }
636         }
637 
buildExcludeDirClause(int count)638         private String buildExcludeDirClause(int count) {
639             if (count == 0) {
640                 return "";
641             }
642             String notLikeClause = FileColumns.DATA + " NOT LIKE ? ESCAPE '\\'";
643             String andClause = " AND ";
644             StringBuilder sb = new StringBuilder();
645             sb.append("(");
646             for (int i = 0; i < count; i++) {
647                 // Append twice because we want to match the path itself and the expanded path
648                 // using the SQL % LIKE operator. For instance, to exclude /sdcard/foo and all
649                 // subdirs, we need the following:
650                 // "NOT LIKE '/sdcard/foo/%' AND "NOT LIKE '/sdcard/foo'"
651                 // The first clause matches *just* subdirs, and the second clause matches the dir
652                 // itself
653                 sb.append(notLikeClause);
654                 sb.append(andClause);
655                 sb.append(notLikeClause);
656                 if (i != count - 1) {
657                     sb.append(andClause);
658                 }
659             }
660             sb.append(")");
661             return sb.toString();
662         }
663 
addEscapedAndExpandedPath(String path, List<String> paths)664         private void addEscapedAndExpandedPath(String path, List<String> paths) {
665             String escapedPath = DatabaseUtils.escapeForLike(path);
666             paths.add(escapedPath + "/%");
667             paths.add(escapedPath);
668         }
669 
buildSqlSelectionArgs()670         private String[] buildSqlSelectionArgs() {
671             List<String> escapedPaths = new ArrayList<>();
672 
673             addEscapedAndExpandedPath(mRoot.getAbsolutePath(), escapedPaths);
674             for (String dir : mExcludeDirs) {
675                 addEscapedAndExpandedPath(dir, escapedPaths);
676             }
677 
678             return escapedPaths.toArray(new String[0]);
679         }
680 
reconcileAndClean()681         private void reconcileAndClean() {
682             final long[] scannedIds = mScannedIds.toArray();
683             Arrays.sort(scannedIds);
684 
685             // The query phase is split from the delete phase so that our query
686             // remains stable if we need to paginate across multiple windows.
687             mSignal.throwIfCanceled();
688             Trace.beginSection("Scanner.reconcile");
689 
690             // Ignore abstract playlists which don't have files on disk
691             final String formatClause = "ifnull(" + FileColumns.FORMAT + ","
692                     + MtpConstants.FORMAT_UNDEFINED + ") != "
693                     + MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST;
694             final String dataClause = "(" + FileColumns.DATA + " LIKE ? ESCAPE '\\' OR "
695                     + FileColumns.DATA + " LIKE ? ESCAPE '\\')";
696             final String excludeDirClause = buildExcludeDirClause(mExcludeDirs.size());
697             final String generationClause = FileColumns.GENERATION_ADDED + " <= "
698                     + mStartGeneration;
699             final String sqlSelection = formatClause + " AND " + dataClause + " AND "
700                     + generationClause
701                     + (excludeDirClause.isEmpty() ? "" : " AND " + excludeDirClause);
702             final Bundle queryArgs = new Bundle();
703             queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, sqlSelection);
704             queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
705                     buildSqlSelectionArgs());
706             queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
707                     FileColumns._ID + " DESC");
708             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
709             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
710             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
711 
712             int[] countPerMediaType;
713             try {
714                 countPerMediaType = addUnknownIdsAndGetMediaTypeCount(queryArgs, scannedIds);
715             } catch (SQLiteBlobTooBigException e) {
716                 // Catching SQLiteBlobTooBigException to avoid MP process crash. There can be two
717                 // scenarios where SQLiteBlobTooBigException is thrown.
718                 // First, where data read by cursor is more than 2MB size. In this case,
719                 // next fill window request might try to read data which may not exist anymore due
720                 // to a recent update after the last query.
721                 // Second, when columns being read have total size of more than 2MB.
722                 // We intend to solve for first scenario by querying MP again. If the initial
723                 // failure was because of second scenario, a runtime exception will be thrown.
724                 Log.e(TAG, "Encountered exception: ", e);
725                 mUnknownIds.clear();
726                 countPerMediaType = addUnknownIdsAndGetMediaTypeCount(queryArgs, scannedIds);
727             } finally {
728                 Trace.endSection();
729             }
730 
731             // Third, clean all the unknown database entries found above
732             mSignal.throwIfCanceled();
733             Trace.beginSection("Scanner.clean");
734             try {
735                 for (int i = 0; i < mUnknownIds.size(); i++) {
736                     final long id = mUnknownIds.get(i);
737                     if (LOGV) Log.v(TAG, "Cleaning " + id);
738                     final Uri uri = MediaStore.Files.getContentUri(mVolumeName, id).buildUpon()
739                             .appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false")
740                             .build();
741                     addPending(ContentProviderOperation.newDelete(uri).build());
742                     maybeApplyPending();
743                 }
744                 applyPending();
745             } finally {
746                 if (mUnknownIds.size() > 0) {
747                     String scanReason = "scan triggered by reason: " + translateReason(mReason);
748                     Metrics.logDeletionPersistent(mVolumeName, scanReason, countPerMediaType);
749                 }
750                 Trace.endSection();
751             }
752         }
753 
addUnknownIdsAndGetMediaTypeCount(Bundle queryArgs, long[] scannedIds)754         private int[] addUnknownIdsAndGetMediaTypeCount(Bundle queryArgs, long[] scannedIds) {
755             int[] countPerMediaType = new int[FileColumns.MEDIA_TYPE_COUNT];
756             try (Cursor c = mResolver.query(mFilesUri,
757                     new String[]{FileColumns._ID, FileColumns.MEDIA_TYPE, FileColumns.DATE_EXPIRES,
758                             FileColumns.IS_PENDING}, queryArgs, mSignal)) {
759                 while (c.moveToNext()) {
760                     final long id = c.getLong(0);
761                     if (Arrays.binarySearch(scannedIds, id) < 0) {
762                         final long dateExpire = c.getLong(2);
763                         final boolean isPending = c.getInt(3) == 1;
764                         // Don't delete the pending item which is not expired.
765                         // If the scan is triggered between invoking
766                         // ContentResolver#insert() and ContentResolver#openFileDescriptor(),
767                         // it raises the FileNotFoundException b/166063754.
768                         if (isPending && dateExpire > System.currentTimeMillis() / 1000) {
769                             continue;
770                         }
771                         mUnknownIds.add(id);
772                         final int mediaType = c.getInt(1);
773                         // Avoid ArrayIndexOutOfBounds if more mediaTypes are added,
774                         // but mediaTypeSize is not updated
775                         if (mediaType < countPerMediaType.length) {
776                             countPerMediaType[mediaType]++;
777                         }
778                     }
779                 }
780             }
781 
782             return countPerMediaType;
783         }
784 
resolvePlaylists()785         private void resolvePlaylists() {
786             mSignal.throwIfCanceled();
787 
788             // Playlists aren't supported on internal storage, so bail early
789             if (MediaStore.VOLUME_INTERNAL.equals(mVolumeName)) return;
790 
791             final Uri playlistsUri = MediaStore.Audio.Playlists.getContentUri(mVolumeName);
792             final Bundle queryArgs = new Bundle();
793             queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
794                     FileColumns.GENERATION_MODIFIED + " > " + mStartGeneration);
795             try (Cursor c = mResolver.query(playlistsUri, new String[] { FileColumns._ID },
796                     queryArgs, mSignal)) {
797                 while (c.moveToNext()) {
798                     final long id = c.getLong(0);
799                     MediaStore.resolvePlaylistMembers(mResolver,
800                             ContentUris.withAppendedId(playlistsUri, id));
801                 }
802             } finally {
803                 Trace.endSection();
804             }
805         }
806 
807         /**
808          * Create and acquire a lock on the given directory, giving the calling
809          * thread exclusive access to ensure that parallel scans don't overlap
810          * and confuse each other.
811          */
acquireDirectoryLock(@onNull String dirPath)812         private void acquireDirectoryLock(@NonNull String dirPath) {
813             Trace.beginSection("Scanner.acquireDirectoryLock");
814             DirectoryLock lock;
815             final String dirLower = dirPath.toLowerCase(Locale.ROOT);
816             synchronized (mDirectoryLocks) {
817                 lock = mDirectoryLocks.get(dirLower);
818                 if (lock == null) {
819                     lock = new DirectoryLock();
820                     mDirectoryLocks.put(dirLower, lock);
821                 }
822                 lock.count++;
823             }
824             lock.lock.lock();
825             mAcquiredDirectoryLocks.add(dirLower);
826             Trace.endSection();
827         }
828 
829         /**
830          * Release a currently held lock on the given directory, releasing any
831          * other waiting parallel scans to proceed, and cleaning up data
832          * structures if no other threads are waiting.
833          */
releaseDirectoryLock(@onNull String dirPath)834         private void releaseDirectoryLock(@NonNull String dirPath) {
835             Trace.beginSection("Scanner.releaseDirectoryLock");
836             DirectoryLock lock;
837             final String dirLower = dirPath.toLowerCase(Locale.ROOT);
838             synchronized (mDirectoryLocks) {
839                 lock = mDirectoryLocks.get(dirLower);
840                 if (lock == null) {
841                     throw new IllegalStateException();
842                 }
843                 if (--lock.count == 0) {
844                     mDirectoryLocks.remove(dirLower);
845                 }
846             }
847             lock.lock.unlock();
848             mAcquiredDirectoryLocks.remove(dirLower);
849             Trace.endSection();
850         }
851 
852         @Override
close()853         public void close() {
854             // Release any locks we're still holding, typically when we
855             // encountered an exception; we snapshot the original list so we're
856             // not confused as it's mutated by release operations
857             for (String dirPath : new ArraySet<>(mAcquiredDirectoryLocks)) {
858                 releaseDirectoryLock(dirPath);
859             }
860 
861             mClient.close();
862         }
863 
864         @Override
preVisitDirectory(Path dir, BasicFileAttributes attrs)865         public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
866                 throws IOException {
867             // Possibly bail before digging into each directory
868             mSignal.throwIfCanceled();
869 
870             if (!shouldScanDirectory(dir.toFile())) {
871                 return FileVisitResult.SKIP_SUBTREE;
872             }
873 
874             synchronized (mPendingCleanDirectories) {
875                 if (mIsDirectoryTreeDirty) {
876                     // Directory tree is dirty, continue scanning subtree.
877                 } else if (FileUtils.getTopLevelNoMedia(dir.toFile()) == null) {
878                   // No nomedia file found, continue scanning.
879                 } else if (FileUtils.isDirectoryDirty(FileUtils.getTopLevelNoMedia(dir.toFile()))) {
880                     // Track the directory dirty status for directory tree in mIsDirectoryDirty.
881                     // This removes additional dirty state check for subdirectories of nomedia
882                     // directory.
883                     mIsDirectoryTreeDirty = true;
884                     mPendingCleanDirectories.add(dir.toFile().getPath().toLowerCase(Locale.ROOT));
885                 } else {
886                     Log.d(TAG, "Skipping preVisitDirectory " + dir.toFile());
887                     if (mExcludeDirs.size() <= MAX_EXCLUDE_DIRS) {
888                         mExcludeDirs.add(dir.toFile().getPath().toLowerCase(Locale.ROOT));
889                         return FileVisitResult.SKIP_SUBTREE;
890                     } else {
891                         Log.w(TAG, "ExcludeDir size exceeded, not skipping preVisitDirectory "
892                                 + dir.toFile());
893                     }
894                 }
895             }
896 
897             // Acquire lock on this directory to ensure parallel scans don't
898             // overlap and confuse each other
899             acquireDirectoryLock(dir.toString());
900 
901             if (!mIsDirectoryTreeHidden && FileUtils.isDirectoryHidden(dir.toFile())) {
902                 mIsDirectoryTreeHidden = true;
903                 mTopLevelHiddenDirectory = dir.toString();
904             }
905 
906             // Scan this directory as a normal file so that "parent" database
907             // entries are created
908             return visitFile(dir, attrs);
909         }
910 
911         @Override
visitFile(Path file, BasicFileAttributes attrs)912         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
913                 throws IOException {
914             if (LOGV) Log.v(TAG, "Visiting " + file);
915             mFileCount++;
916 
917             // Skip files that have already been scanned, and which haven't
918             // changed since they were last scanned
919             final File realFile = file.toFile();
920             long existingId = -1;
921 
922             String actualMimeType;
923             if (attrs.isDirectory()) {
924                 actualMimeType = null;
925             } else {
926                 actualMimeType = MimeUtils.resolveMimeType(realFile);
927             }
928 
929             // Resolve the MIME type of DRM files before scanning them; if we
930             // have trouble then we'll continue scanning as a generic file
931             final boolean isDrm = mDrmMimeTypes.contains(actualMimeType);
932             if (isDrm) {
933                 actualMimeType = mDrmClient.getOriginalMimeType(realFile.getPath());
934             }
935 
936             int actualMediaType = mediaTypeFromMimeType(
937                     realFile, actualMimeType, FileColumns.MEDIA_TYPE_NONE);
938 
939             Trace.beginSection("Scanner.checkChanged");
940 
941             final Bundle queryArgs = new Bundle();
942             queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
943                     FileColumns.DATA + "=?");
944             queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
945                     new String[] { realFile.getAbsolutePath() });
946             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
947             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
948             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
949             final String[] projection = new String[] {FileColumns._ID, FileColumns.DATE_MODIFIED,
950                     FileColumns.SIZE, FileColumns.MIME_TYPE, FileColumns.MEDIA_TYPE,
951                     FileColumns.IS_PENDING, FileColumns._MODIFIER};
952 
953             final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(realFile.getName());
954             // If IS_PENDING is set by FUSE, we should scan the file and update IS_PENDING to zero.
955             // Pending files from FUSE will not be rewritten to contain expiry timestamp.
956             boolean isPendingFromFuse = !matcher.matches();
957             boolean shouldKeepGenerationUnchanged = false;
958 
959             try (Cursor c = mResolver.query(mFilesUri, projection, queryArgs, mSignal)) {
960                 if (c.moveToFirst()) {
961                     existingId = c.getLong(0);
962                     final String mimeType = c.getString(3);
963                     final int mediaType = c.getInt(4);
964                     isPendingFromFuse &= c.getInt(5) != 0;
965 
966                     // Remember visiting this existing item, even if we skipped
967                     // due to it being unchanged; this is needed so we don't
968                     // delete the item during a later cleaning phase
969                     mScannedIds.add(existingId);
970 
971                     // We also technically found our first result
972                     if (mFirstId == -1) {
973                         mFirstId = existingId;
974                     }
975 
976                     if (attrs.isDirectory()) {
977                         if (LOGV) Log.v(TAG, "Skipping directory " + file);
978                         return FileVisitResult.CONTINUE;
979                     }
980 
981                     final boolean sameMetadata =
982                             hasSameMetadata(attrs, realFile, isPendingFromFuse, c);
983                     final boolean sameMediaType = actualMediaType == mediaType;
984                     if (sameMetadata && sameMediaType) {
985                         if (LOGV) Log.v(TAG, "Skipping unchanged " + file);
986                         return FileVisitResult.CONTINUE;
987                     }
988 
989                     // For this special case we may have changed mime type from the file's metadata.
990                     // This is safe because mime_type cannot be changed outside of scanning.
991                     if (sameMetadata
992                             && "video/mp4".equalsIgnoreCase(actualMimeType)
993                             && "audio/mp4".equalsIgnoreCase(mimeType)) {
994                         if (LOGV) Log.v(TAG, "Skipping unchanged video/audio " + file);
995                         return FileVisitResult.CONTINUE;
996                     }
997 
998                     if ((Flags.audioSampleColumns() || Flags.inferredMediaDate())
999                             && mReason == REASON_IDLE
1000                             && c.getInt(6) == FileColumns._MODIFIER_SCHEMA_UPDATE) {
1001                         shouldKeepGenerationUnchanged = true;
1002                     }
1003                 }
1004 
1005 
1006                 // Since we allow top-level mime type to be customised, we need to do this early
1007                 // on, so the file is later scanned as the appropriate type (otherwise, this
1008                 // audio filed would be scanned as video and it would be missing the correct
1009                 // metadata).
1010                 actualMimeType = updateM4aMimeType(realFile, actualMimeType);
1011                 actualMediaType =
1012                         mediaTypeFromMimeType(realFile, actualMimeType, actualMediaType);
1013             } finally {
1014                 Trace.endSection();
1015             }
1016 
1017             final ContentProviderOperation.Builder op;
1018             Trace.beginSection("Scanner.scanItem");
1019             try {
1020                 op = scanItem(existingId, realFile, attrs, actualMimeType, actualMediaType,
1021                         mVolumeName, mRestoreExecutorOptional.orElse(null));
1022             } finally {
1023                 Trace.endSection();
1024             }
1025             if (op != null) {
1026                 op.withValue(FileColumns._MODIFIER, FileColumns._MODIFIER_MEDIA_SCAN);
1027 
1028                 // Flag we do not want generation modified if it's an idle scan update
1029                 if ((Flags.audioSampleColumns() || Flags.inferredMediaDate())
1030                         && shouldKeepGenerationUnchanged) {
1031                     op.withValue(FileColumns.GENERATION_MODIFIED,
1032                             FileColumns.GENERATION_MODIFIED_UNCHANGED);
1033                 }
1034 
1035                 // Force DRM files to be marked as DRM, since the lower level
1036                 // stack may not set this correctly
1037                 if (isDrm) {
1038                     op.withValue(MediaColumns.IS_DRM, 1);
1039                 }
1040 
1041                 if (enableOemMetadata()) {
1042                     if (mOemSupportedMimeTypes == null) {
1043                         mOemSupportedMimeTypes = getOemSupportedMimeTypes();
1044                     }
1045                     if (mOemSupportedMimeTypes.contains(actualMimeType)) {
1046                         // If mime type is supported by OEM
1047                         fetchOemMetadata(op, realFile);
1048                     }
1049                 }
1050 
1051                 addPending(op.build());
1052                 maybeApplyPending();
1053             }
1054             return FileVisitResult.CONTINUE;
1055         }
1056 
fetchOemMetadata(ContentProviderOperation.Builder op, File file)1057         private void fetchOemMetadata(ContentProviderOperation.Builder op, File file) {
1058             if (!enableOemMetadata()) {
1059                 return;
1060             }
1061             try {
1062                 // Return if no package implements OemMetadataService
1063                 if (!mDefaultOemMetadataServicePackage.isPresent()) {
1064                     return;
1065                 }
1066 
1067                 if (mOemMetadataServiceWrapper == null) {
1068                     connectOemMetadataServiceWrapper();
1069                 }
1070 
1071                 // Return if we cannot find any connection
1072                 if (mOemMetadataServiceWrapper == null) {
1073                     return;
1074                 }
1075 
1076                 try (ParcelFileDescriptor pfd = FileUtils.openSafely(file,
1077                         ParcelFileDescriptor.MODE_READ_ONLY)) {
1078                     Map<String, String> oemMetadata = mOemMetadataServiceWrapper.getOemCustomData(
1079                             pfd);
1080                     op.withValue(FileColumns.OEM_METADATA, oemMetadata.toString().getBytes());
1081                     Log.v(TAG, "Fetched OEM metadata successfully");
1082                 } catch (Exception e) {
1083                     Log.w(TAG, "Failure in fetching OEM metadata", e);
1084                 }
1085             } catch (Exception e) {
1086                 Log.w(TAG, "Failure in connecting to OEM metadata service", e);
1087             }
1088         }
1089 
mediaTypeFromMimeType( File file, String mimeType, int defaultMediaType)1090         private int mediaTypeFromMimeType(
1091                 File file, String mimeType, int defaultMediaType) {
1092             if (mimeType != null) {
1093                 return resolveMediaTypeFromFilePath(
1094                         file, mimeType, /*isHidden*/ mIsDirectoryTreeHidden);
1095             }
1096             return defaultMediaType;
1097         }
1098 
hasSameMetadata( BasicFileAttributes attrs, File realFile, boolean isPendingFromFuse, Cursor c)1099         private boolean hasSameMetadata(
1100                 BasicFileAttributes attrs, File realFile, boolean isPendingFromFuse, Cursor c) {
1101             final long dateModified = c.getLong(1);
1102             final boolean sameTime = (lastModifiedTime(realFile, attrs) == dateModified);
1103 
1104             final long size = c.getLong(2);
1105             final boolean sameSize = (attrs.size() == size);
1106 
1107             final int modifier = c.getInt(6);
1108             final boolean isScanned =
1109                     modifier == FileColumns._MODIFIER_MEDIA_SCAN
1110                             // We scan a file after the schema update only on idle maintenance
1111                             || (modifier == FileColumns._MODIFIER_SCHEMA_UPDATE
1112                             && mReason != REASON_IDLE);
1113 
1114             return sameTime && sameSize && !isPendingFromFuse && isScanned;
1115         }
1116 
1117         /**
1118          * For this one very narrow case, we allow mime types to be customised when the top levels
1119          * differ. This opens the given file, so avoid calling unless really necessary. This
1120          * returns the defaultMimeType for non-m4a files or if opening the file throws an exception.
1121          */
updateM4aMimeType(File file, String defaultMimeType)1122         private String updateM4aMimeType(File file, String defaultMimeType) {
1123             if ("video/mp4".equalsIgnoreCase(defaultMimeType)) {
1124                 try (
1125                     FileInputStream is = new FileInputStream(file);
1126                     MediaMetadataRetriever mmr = new MediaMetadataRetriever()) {
1127                     mmr.setDataSource(is.getFD());
1128                     String refinedMimeType = mmr.extractMetadata(METADATA_KEY_MIMETYPE);
1129                     if ("audio/mp4".equalsIgnoreCase(refinedMimeType)) {
1130                         return refinedMimeType;
1131                     }
1132                 } catch (Exception e) {
1133                     return defaultMimeType;
1134                 }
1135             }
1136             return defaultMimeType;
1137         }
1138 
1139         @Override
visitFileFailed(Path file, IOException exc)1140         public FileVisitResult visitFileFailed(Path file, IOException exc)
1141                 throws IOException {
1142             Log.w(TAG, "Failed to visit " + file + ": " + exc);
1143             return FileVisitResult.CONTINUE;
1144         }
1145 
1146         @Override
postVisitDirectory(Path dir, IOException exc)1147         public FileVisitResult postVisitDirectory(Path dir, IOException exc)
1148                 throws IOException {
1149             // We need to drain all pending changes related to this directory
1150             // before releasing our lock below
1151             applyPending();
1152 
1153             boolean isDirHidden = FileUtils.isDirectoryHidden(dir.toFile());
1154 
1155             if (isDirHidden && !mIsDirectoryTreeHidden) {
1156                 Log.w(TAG, "Hidden state of directory " + dir + " changed during active scan.");
1157             }
1158 
1159             if (mTopLevelHiddenDirectory != null && dir.toString().equals(
1160                     mTopLevelHiddenDirectory)) {
1161                 // Post visit the top level hidden directory being tracked. Reset hidden status
1162                 // for directory tree.
1163                 mIsDirectoryTreeHidden = false;
1164                 mTopLevelHiddenDirectory = null;
1165             }
1166 
1167             // Now that we're finished scanning this directory, release lock to
1168             // allow other parallel scans to proceed
1169             releaseDirectoryLock(dir.toString());
1170 
1171             if (mIsDirectoryTreeDirty) {
1172                 synchronized (mPendingCleanDirectories) {
1173                     if (mPendingCleanDirectories.remove(
1174                             dir.toFile().getPath().toLowerCase(Locale.ROOT))) {
1175                         // If |dir| is still clean, then persist
1176                         FileUtils.setDirectoryDirty(dir.toFile(), false /* isDirty */);
1177                         if (!MediaStore.VOLUME_INTERNAL.equals(mVolumeName)) {
1178                             invalidateFuseDentryInBg(dir.toFile());
1179                         }
1180                         mIsDirectoryTreeDirty = false;
1181                     }
1182                 }
1183             }
1184             return FileVisitResult.CONTINUE;
1185         }
1186 
addPending(@onNull ContentProviderOperation op)1187         private void addPending(@NonNull ContentProviderOperation op) {
1188             mPending.add(op);
1189 
1190             if (op.isInsert()) mInsertCount++;
1191             if (op.isUpdate()) mUpdateCount++;
1192             if (op.isDelete()) mDeleteCount++;
1193         }
1194 
maybeApplyPending()1195         private void maybeApplyPending() {
1196             if (mPending.size() > BATCH_SIZE) {
1197                 applyPending();
1198             }
1199         }
1200 
applyPending()1201         private void applyPending() {
1202             // Bail early when nothing pending
1203             if (mPending.isEmpty()) return;
1204 
1205             Trace.beginSection("Scanner.applyPending");
1206             try {
1207                 ContentProviderResult[] results = mResolver.applyBatch(AUTHORITY, mPending);
1208                 for (int index = 0; index < results.length; index++) {
1209                     ContentProviderResult result = results[index];
1210                     ContentProviderOperation operation = mPending.get(index);
1211 
1212                     if (result.exception != null) {
1213                         Log.w(TAG, "Failed to apply " + operation, result.exception);
1214                     }
1215 
1216                     Uri uri = result.uri;
1217                     if (uri != null) {
1218                         final long id = ContentUris.parseId(uri);
1219                         if (mFirstId == -1) {
1220                             mFirstId = id;
1221                         }
1222                         mScannedIds.add(id);
1223                     }
1224                 }
1225             } catch (RemoteException | OperationApplicationException e) {
1226                 Log.w(TAG, "Failed to apply", e);
1227             } finally {
1228                 mPending.clear();
1229                 Trace.endSection();
1230             }
1231         }
1232 
1233         /**
1234          * Return the first item encountered by this scan requested.
1235          * <p>
1236          * Internally resolves to the relevant media collection where this item
1237          * exists based on {@link FileColumns#MEDIA_TYPE}.
1238          */
getFirstResult()1239         public @Nullable Uri getFirstResult() {
1240             if (mFirstId == -1) return null;
1241 
1242             final Uri fileUri = MediaStore.Files.getContentUri(mVolumeName, mFirstId);
1243             try (Cursor c = mResolver.query(fileUri,
1244                     new String[] { FileColumns.MEDIA_TYPE }, null, null)) {
1245                 if (c.moveToFirst()) {
1246                     switch (c.getInt(0)) {
1247                         case FileColumns.MEDIA_TYPE_AUDIO:
1248                             return MediaStore.Audio.Media.getContentUri(mVolumeName, mFirstId);
1249                         case FileColumns.MEDIA_TYPE_VIDEO:
1250                             return MediaStore.Video.Media.getContentUri(mVolumeName, mFirstId);
1251                         case FileColumns.MEDIA_TYPE_IMAGE:
1252                             return MediaStore.Images.Media.getContentUri(mVolumeName, mFirstId);
1253                         case FileColumns.MEDIA_TYPE_PLAYLIST:
1254                             return ContentUris.withAppendedId(
1255                                     MediaStore.Audio.Playlists.getContentUri(mVolumeName),
1256                                     mFirstId);
1257                     }
1258                 }
1259             }
1260 
1261             // Worst case, we can always use generic collection
1262             return fileUri;
1263         }
1264     }
1265 
1266     /**
1267      * Scan the requested file, returning a {@link ContentProviderOperation}
1268      * containing all indexed metadata, suitable for passing to a
1269      * {@link SQLiteDatabase#replace} operation.
1270      */
scanItem(long existingId, File file, BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName, RestoreExecutor restoreExecutor)1271     private @Nullable ContentProviderOperation.Builder scanItem(long existingId, File file,
1272             BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName,
1273             RestoreExecutor restoreExecutor) {
1274         if (Objects.equals(file.getName(), ".nomedia")) {
1275             if (LOGD) Log.d(TAG, "Ignoring .nomedia file: " + file);
1276             return null;
1277         }
1278 
1279         if (attrs.isDirectory()) {
1280             return scanItemDirectory(existingId, file, attrs, mimeType, volumeName);
1281         }
1282 
1283         // Recovery is performed on first scan of file in target device
1284         try {
1285             if (restoreExecutor != null) {
1286                 Optional<ContentValues> restoredDataOptional = restoreExecutor
1287                         .getMetadataForFileIfBackedUp(file.getAbsolutePath(), mContext);
1288                 if (restoredDataOptional.isPresent()) {
1289                     ContentValues valuesRestored = restoredDataOptional.get();
1290                     if (isRestoredMetadataOfActualFile(valuesRestored, attrs)) {
1291                         return restoreDataFromBackup(valuesRestored, file, attrs, mimeType,
1292                                 existingId);
1293                     }
1294                 }
1295             }
1296         } catch (Exception e) {
1297             Log.e(TAG, "Error while attempting to restore metadata from backup", e);
1298         }
1299 
1300         switch (mediaType) {
1301             case FileColumns.MEDIA_TYPE_AUDIO:
1302                 return scanItemAudio(existingId, file, attrs, mimeType, mediaType, volumeName);
1303             case FileColumns.MEDIA_TYPE_VIDEO:
1304                 return scanItemVideo(existingId, file, attrs, mimeType, mediaType, volumeName);
1305             case FileColumns.MEDIA_TYPE_IMAGE:
1306                 return scanItemImage(existingId, file, attrs, mimeType, mediaType, volumeName);
1307             case FileColumns.MEDIA_TYPE_PLAYLIST:
1308                 return scanItemPlaylist(existingId, file, attrs, mimeType, mediaType, volumeName);
1309             case FileColumns.MEDIA_TYPE_SUBTITLE:
1310                 return scanItemSubtitle(existingId, file, attrs, mimeType, mediaType, volumeName);
1311             case FileColumns.MEDIA_TYPE_DOCUMENT:
1312                 return scanItemDocument(existingId, file, attrs, mimeType, mediaType, volumeName);
1313             default:
1314                 return scanItemFile(existingId, file, attrs, mimeType, mediaType, volumeName);
1315         }
1316     }
1317 
isRestoredMetadataOfActualFile(@onNull ContentValues contentValues, BasicFileAttributes attrs)1318     private boolean isRestoredMetadataOfActualFile(@NonNull ContentValues contentValues,
1319             BasicFileAttributes attrs) {
1320         long actualFileSize = attrs.size();
1321         String fileSizeFromBackup = contentValues.getAsString(MediaStore.Files.FileColumns.SIZE);
1322         if (fileSizeFromBackup == null) {
1323             return false;
1324         }
1325 
1326         return actualFileSize == Long.parseLong(fileSizeFromBackup);
1327     }
1328 
restoreDataFromBackup( ContentValues restoredValues, File file, BasicFileAttributes attrs, String mimeType, long existingId)1329     private ContentProviderOperation.Builder restoreDataFromBackup(
1330             ContentValues restoredValues, File file, BasicFileAttributes attrs, String mimeType,
1331             long existingId) {
1332         final ContentProviderOperation.Builder op = newUpsert(VOLUME_EXTERNAL, existingId);
1333         withGenericValues(op, file, attrs, mimeType, /* mediaType */ null);
1334         op.withValues(restoredValues);
1335         return op;
1336     }
1337 
1338     /**
1339      * Populate the given {@link ContentProviderOperation} with the generic
1340      * {@link MediaColumns} values that can be determined directly from the file
1341      * or its attributes.
1342      * <p>
1343      * This is typically the first set of values defined so that we correctly
1344      * clear any values that had been set by a previous scan and which are no
1345      * longer present in the media item.
1346      */
withGenericValues(ContentProviderOperation.Builder op, File file, BasicFileAttributes attrs, String mimeType, Integer mediaType)1347     private void withGenericValues(ContentProviderOperation.Builder op,
1348             File file, BasicFileAttributes attrs, String mimeType, Integer mediaType) {
1349         withOptionalMimeTypeAndMediaType(op, Optional.ofNullable(mimeType),
1350                 Optional.ofNullable(mediaType));
1351 
1352         op.withValue(MediaColumns.DATA, file.getAbsolutePath());
1353         op.withValue(MediaColumns.SIZE, attrs.size());
1354         op.withValue(MediaColumns.DATE_MODIFIED, lastModifiedTime(file, attrs));
1355         op.withValue(MediaColumns.DATE_TAKEN, null);
1356         op.withValue(MediaColumns.IS_DRM, 0);
1357         op.withValue(MediaColumns.WIDTH, null);
1358         op.withValue(MediaColumns.HEIGHT, null);
1359         op.withValue(MediaColumns.RESOLUTION, null);
1360         op.withValue(MediaColumns.DOCUMENT_ID, null);
1361         op.withValue(MediaColumns.INSTANCE_ID, null);
1362         op.withValue(MediaColumns.ORIGINAL_DOCUMENT_ID, null);
1363         op.withValue(MediaColumns.ORIENTATION, null);
1364 
1365         op.withValue(MediaColumns.CD_TRACK_NUMBER, null);
1366         op.withValue(MediaColumns.ALBUM, null);
1367         op.withValue(MediaColumns.ARTIST, null);
1368         op.withValue(MediaColumns.AUTHOR, null);
1369         op.withValue(MediaColumns.COMPOSER, null);
1370         op.withValue(MediaColumns.GENRE, null);
1371         op.withValue(MediaColumns.TITLE, FileUtils.extractFileName(file.getName()));
1372         op.withValue(MediaColumns.YEAR, null);
1373         op.withValue(MediaColumns.DURATION, null);
1374         op.withValue(MediaColumns.NUM_TRACKS, null);
1375         op.withValue(MediaColumns.WRITER, null);
1376         op.withValue(MediaColumns.ALBUM_ARTIST, null);
1377         op.withValue(MediaColumns.DISC_NUMBER, null);
1378         op.withValue(MediaColumns.COMPILATION, null);
1379         op.withValue(MediaColumns.BITRATE, null);
1380         op.withValue(MediaColumns.CAPTURE_FRAMERATE, null);
1381     }
1382 
1383     /**
1384      * Populate the given {@link ContentProviderOperation} with the generic
1385      * {@link MediaColumns} values using the given
1386      * {@link MediaMetadataRetriever}.
1387      */
withRetrieverValues(ContentProviderOperation.Builder op, MediaMetadataRetriever mmr, String mimeType)1388     private void withRetrieverValues(ContentProviderOperation.Builder op,
1389             MediaMetadataRetriever mmr, String mimeType) {
1390         withOptionalMimeTypeAndMediaType(op,
1391                 parseOptionalMimeType(mimeType, mmr.extractMetadata(METADATA_KEY_MIMETYPE)),
1392                 /*optionalMediaType*/ Optional.empty());
1393 
1394         withOptionalValue(op, MediaColumns.DATE_TAKEN,
1395                 parseOptionalDate(mmr.extractMetadata(METADATA_KEY_DATE)));
1396         withOptionalValue(op, MediaColumns.CD_TRACK_NUMBER,
1397                 parseOptional(mmr.extractMetadata(METADATA_KEY_CD_TRACK_NUMBER)));
1398         withOptionalValue(op, MediaColumns.ALBUM,
1399                 parseOptional(mmr.extractMetadata(METADATA_KEY_ALBUM)));
1400         withOptionalValue(op, MediaColumns.ARTIST, firstPresent(
1401                 parseOptional(mmr.extractMetadata(METADATA_KEY_ARTIST)),
1402                 parseOptional(mmr.extractMetadata(METADATA_KEY_ALBUMARTIST))));
1403         withOptionalValue(op, MediaColumns.AUTHOR,
1404                 parseOptional(mmr.extractMetadata(METADATA_KEY_AUTHOR)));
1405         withOptionalValue(op, MediaColumns.COMPOSER,
1406                 parseOptional(mmr.extractMetadata(METADATA_KEY_COMPOSER)));
1407         withOptionalValue(op, MediaColumns.GENRE,
1408                 parseOptional(mmr.extractMetadata(METADATA_KEY_GENRE)));
1409         withOptionalValue(op, MediaColumns.TITLE,
1410                 parseOptional(mmr.extractMetadata(METADATA_KEY_TITLE)));
1411         withOptionalValue(op, MediaColumns.YEAR,
1412                 parseOptionalYear(mmr.extractMetadata(METADATA_KEY_YEAR)));
1413         withOptionalValue(op, MediaColumns.DURATION,
1414                 parseOptional(mmr.extractMetadata(METADATA_KEY_DURATION)));
1415         withOptionalValue(op, MediaColumns.NUM_TRACKS,
1416                 parseOptional(mmr.extractMetadata(METADATA_KEY_NUM_TRACKS)));
1417         withOptionalValue(op, MediaColumns.WRITER,
1418                 parseOptional(mmr.extractMetadata(METADATA_KEY_WRITER)));
1419         withOptionalValue(op, MediaColumns.ALBUM_ARTIST,
1420                 parseOptional(mmr.extractMetadata(METADATA_KEY_ALBUMARTIST)));
1421         withOptionalValue(op, MediaColumns.DISC_NUMBER,
1422                 parseOptional(mmr.extractMetadata(METADATA_KEY_DISC_NUMBER)));
1423         withOptionalValue(op, MediaColumns.COMPILATION,
1424                 parseOptional(mmr.extractMetadata(METADATA_KEY_COMPILATION)));
1425         withOptionalValue(op, MediaColumns.BITRATE,
1426                 parseOptional(mmr.extractMetadata(METADATA_KEY_BITRATE)));
1427         withOptionalValue(op, MediaColumns.CAPTURE_FRAMERATE,
1428                 parseOptional(mmr.extractMetadata(METADATA_KEY_CAPTURE_FRAMERATE)));
1429     }
1430 
1431     /**
1432      * Populate the given {@link ContentProviderOperation} with the generic
1433      * {@link MediaColumns} values using the given XMP metadata.
1434      */
withXmpValues(ContentProviderOperation.Builder op, XmpInterface xmp, String mimeType)1435     private void withXmpValues(ContentProviderOperation.Builder op,
1436             XmpInterface xmp, String mimeType) {
1437         withOptionalMimeTypeAndMediaType(op,
1438                 parseOptionalMimeType(mimeType, xmp.getFormat()),
1439                 /*optionalMediaType*/ Optional.empty());
1440 
1441         op.withValue(MediaColumns.DOCUMENT_ID, xmp.getDocumentId());
1442         op.withValue(MediaColumns.INSTANCE_ID, xmp.getInstanceId());
1443         op.withValue(MediaColumns.ORIGINAL_DOCUMENT_ID, xmp.getOriginalDocumentId());
1444         op.withValue(MediaColumns.XMP, maybeTruncateXmp(xmp));
1445     }
1446 
maybeTruncateXmp(XmpInterface xmp)1447     private byte[] maybeTruncateXmp(XmpInterface xmp) {
1448         byte[] redacted = xmp.getRedactedXmp();
1449         if (redacted.length > MAX_XMP_SIZE_BYTES) {
1450             return new byte[0];
1451         }
1452 
1453         return redacted;
1454     }
1455 
1456     /**
1457      * Overwrite a value in the given {@link ContentProviderOperation}, but only
1458      * when the given {@link Optional} value is present.
1459      */
withOptionalValue(@onNull ContentProviderOperation.Builder op, @NonNull String key, @NonNull Optional<?> value)1460     private void withOptionalValue(@NonNull ContentProviderOperation.Builder op,
1461             @NonNull String key, @NonNull Optional<?> value) {
1462         if (value.isPresent()) {
1463             op.withValue(key, value.get());
1464         }
1465     }
1466 
1467     /**
1468      * Overwrite the {@link MediaColumns#MIME_TYPE} and
1469      * {@link FileColumns#MEDIA_TYPE} values in the given
1470      * {@link ContentProviderOperation}, but only when the given
1471      * {@link Optional} optionalMimeType is present.
1472      * If {@link Optional} optionalMediaType is not present, {@link FileColumns#MEDIA_TYPE} is
1473      * resolved from given {@code optionalMimeType} when {@code optionalMimeType} is present.
1474      *
1475      * @param optionalMimeType An optional MIME type to apply to this operation.
1476      * @param optionalMediaType An optional Media type to apply to this operation.
1477      */
withOptionalMimeTypeAndMediaType( @onNull ContentProviderOperation.Builder op, @NonNull Optional<String> optionalMimeType, @NonNull Optional<Integer> optionalMediaType)1478     private void withOptionalMimeTypeAndMediaType(
1479             @NonNull ContentProviderOperation.Builder op,
1480             @NonNull Optional<String> optionalMimeType,
1481             @NonNull Optional<Integer> optionalMediaType) {
1482         if (optionalMimeType.isPresent()) {
1483             final String mimeType = optionalMimeType.get();
1484             op.withValue(MediaColumns.MIME_TYPE, mimeType);
1485             if (optionalMediaType.isPresent()) {
1486                 op.withValue(FileColumns.MEDIA_TYPE, optionalMediaType.get());
1487             } else {
1488                 op.withValue(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
1489             }
1490         }
1491     }
1492 
withResolutionValues( @onNull ContentProviderOperation.Builder op, @NonNull ExifInterface exif, @NonNull File file)1493     private void withResolutionValues(
1494             @NonNull ContentProviderOperation.Builder op,
1495             @NonNull ExifInterface exif, @NonNull File file) {
1496         final Optional<?> width = parseOptionalOrZero(
1497                 exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH));
1498         final Optional<?> height = parseOptionalOrZero(
1499                 exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH));
1500         final Optional<String> resolution = parseOptionalResolution(width, height);
1501         if (resolution.isPresent()) {
1502             withOptionalValue(op, MediaColumns.WIDTH, width);
1503             withOptionalValue(op, MediaColumns.HEIGHT, height);
1504             op.withValue(MediaColumns.RESOLUTION, resolution.get());
1505         } else {
1506             withBitmapResolutionValues(op, file);
1507         }
1508     }
1509 
withBitmapResolutionValues( @onNull ContentProviderOperation.Builder op, @NonNull File file)1510     private void withBitmapResolutionValues(
1511             @NonNull ContentProviderOperation.Builder op,
1512             @NonNull File file) {
1513         final BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
1514         bitmapOptions.inSampleSize = 1;
1515         bitmapOptions.inJustDecodeBounds = true;
1516         bitmapOptions.outWidth = 0;
1517         bitmapOptions.outHeight = 0;
1518         BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOptions);
1519 
1520         final Optional<?> width = parseOptionalOrZero(bitmapOptions.outWidth);
1521         final Optional<?> height = parseOptionalOrZero(bitmapOptions.outHeight);
1522         withOptionalValue(op, MediaColumns.WIDTH, width);
1523         withOptionalValue(op, MediaColumns.HEIGHT, height);
1524         withOptionalValue(op, MediaColumns.RESOLUTION, parseOptionalResolution(width, height));
1525     }
1526 
scanItemDirectory(long existingId, File file, BasicFileAttributes attrs, String mimeType, String volumeName)1527     private @NonNull ContentProviderOperation.Builder scanItemDirectory(long existingId,
1528             File file, BasicFileAttributes attrs, String mimeType, String volumeName) {
1529         final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
1530         // Directory doesn't have any MIME type or Media Type.
1531         withGenericValues(op, file, attrs, mimeType, /*mediaType*/ null);
1532 
1533         try {
1534             op.withValue(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
1535         } catch (Exception e) {
1536             logTroubleScanning(file, e);
1537         }
1538         return op;
1539     }
1540 
scanItemAudio(long existingId, File file, BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName)1541     private @NonNull ContentProviderOperation.Builder scanItemAudio(long existingId,
1542             File file, BasicFileAttributes attrs, String mimeType, int mediaType,
1543             String volumeName) {
1544         final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
1545         withGenericValues(op, file, attrs, mimeType, mediaType);
1546 
1547         op.withValue(MediaColumns.ARTIST, UNKNOWN_STRING);
1548         op.withValue(MediaColumns.ALBUM, file.getParentFile().getName());
1549         op.withValue(AudioColumns.TRACK, null);
1550         if (Flags.audioSampleColumns()) {
1551             op.withValue(AudioColumns.BITS_PER_SAMPLE, null);
1552             op.withValue(AudioColumns.SAMPLERATE, null);
1553         }
1554 
1555         FileUtils.computeAudioTypeValuesFromData(file.getAbsolutePath(), op::withValue);
1556 
1557         try (FileInputStream is = new FileInputStream(file)) {
1558             try (MediaMetadataRetriever mmr = new MediaMetadataRetriever()) {
1559                 mmr.setDataSource(is.getFD());
1560 
1561                 withRetrieverValues(op, mmr, mimeType);
1562 
1563                 withOptionalValue(op, AudioColumns.TRACK,
1564                         parseOptionalTrack(mmr));
1565 
1566                 if (Flags.audioSampleColumns() && SdkLevel.isAtLeastT()) {
1567                     withOptionalValue(op, AudioColumns.BITS_PER_SAMPLE,
1568                             parseOptional(mmr.extractMetadata(METADATA_KEY_BITS_PER_SAMPLE)));
1569                     withOptionalValue(op, AudioColumns.SAMPLERATE,
1570                             parseOptional(mmr.extractMetadata(METADATA_KEY_SAMPLERATE)));
1571                 }
1572             }
1573 
1574             // Also hunt around for XMP metadata
1575             final IsoInterface iso = IsoInterface.fromFileDescriptor(is.getFD());
1576             final XmpInterface xmp = XmpDataParser.createXmpInterface(iso);
1577             withXmpValues(op, xmp, mimeType);
1578 
1579         } catch (Exception e) {
1580             logTroubleScanning(file, e);
1581         }
1582         return op;
1583     }
1584 
scanItemPlaylist(long existingId, File file, BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName)1585     private @NonNull ContentProviderOperation.Builder scanItemPlaylist(long existingId,
1586             File file, BasicFileAttributes attrs, String mimeType, int mediaType,
1587             String volumeName) {
1588         final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
1589         withGenericValues(op, file, attrs, mimeType, mediaType);
1590 
1591         try {
1592             op.withValue(PlaylistsColumns.NAME, FileUtils.extractFileName(file.getName()));
1593         } catch (Exception e) {
1594             logTroubleScanning(file, e);
1595         }
1596         return op;
1597     }
1598 
scanItemSubtitle(long existingId, File file, BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName)1599     private @NonNull ContentProviderOperation.Builder scanItemSubtitle(long existingId,
1600             File file, BasicFileAttributes attrs, String mimeType, int mediaType,
1601             String volumeName) {
1602         final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
1603         withGenericValues(op, file, attrs, mimeType, mediaType);
1604 
1605         return op;
1606     }
1607 
scanItemDocument(long existingId, File file, BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName)1608     private @NonNull ContentProviderOperation.Builder scanItemDocument(long existingId,
1609             File file, BasicFileAttributes attrs, String mimeType, int mediaType,
1610             String volumeName) {
1611         final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
1612         withGenericValues(op, file, attrs, mimeType, mediaType);
1613 
1614         return op;
1615     }
1616 
scanItemVideo(long existingId, File file, BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName)1617     private @NonNull ContentProviderOperation.Builder scanItemVideo(long existingId,
1618             File file, BasicFileAttributes attrs, String mimeType, int mediaType,
1619             String volumeName) {
1620         final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
1621         withGenericValues(op, file, attrs, mimeType, mediaType);
1622 
1623         op.withValue(MediaColumns.ARTIST, UNKNOWN_STRING);
1624         op.withValue(MediaColumns.ALBUM, file.getParentFile().getName());
1625         op.withValue(VideoColumns.COLOR_STANDARD, null);
1626         op.withValue(VideoColumns.COLOR_TRANSFER, null);
1627         op.withValue(VideoColumns.COLOR_RANGE, null);
1628         op.withValue(FileColumns._VIDEO_CODEC_TYPE, null);
1629 
1630         try (FileInputStream is = new FileInputStream(file)) {
1631             try (MediaMetadataRetriever mmr = new MediaMetadataRetriever()) {
1632                 mmr.setDataSource(is.getFD());
1633 
1634                 withRetrieverValues(op, mmr, mimeType);
1635 
1636                 withOptionalValue(op, MediaColumns.WIDTH,
1637                         parseOptional(mmr.extractMetadata(METADATA_KEY_VIDEO_WIDTH)));
1638                 withOptionalValue(op, MediaColumns.HEIGHT,
1639                         parseOptional(mmr.extractMetadata(METADATA_KEY_VIDEO_HEIGHT)));
1640                 withOptionalValue(op, MediaColumns.RESOLUTION,
1641                         parseOptionalVideoResolution(mmr));
1642                 withOptionalValue(op, MediaColumns.ORIENTATION,
1643                         parseOptional(mmr.extractMetadata(METADATA_KEY_VIDEO_ROTATION)));
1644 
1645                 withOptionalValue(op, VideoColumns.COLOR_STANDARD,
1646                         parseOptional(mmr.extractMetadata(METADATA_KEY_COLOR_STANDARD)));
1647                 withOptionalValue(op, VideoColumns.COLOR_TRANSFER,
1648                         parseOptional(mmr.extractMetadata(METADATA_KEY_COLOR_TRANSFER)));
1649                 withOptionalValue(op, VideoColumns.COLOR_RANGE,
1650                         parseOptional(mmr.extractMetadata(METADATA_KEY_COLOR_RANGE)));
1651                 withOptionalValue(op, FileColumns._VIDEO_CODEC_TYPE,
1652                         parseOptional(mmr.extractMetadata(METADATA_KEY_VIDEO_CODEC_MIME_TYPE)));
1653 
1654                 // TODO b/373352459 Latitude and Longitude for backup and restore
1655                 // Fill up the latitude and longitude columns
1656                 if (indexMediaLatitudeLongitude()) {
1657                     populateVideoGeolocationCoordinates(op, mmr);
1658                 }
1659             }
1660 
1661             // Also hunt around for XMP metadata
1662             final IsoInterface iso = IsoInterface.fromFileDescriptor(is.getFD());
1663             final XmpInterface xmp = XmpDataParser.createXmpInterface(iso);
1664             withXmpValues(op, xmp, mimeType);
1665 
1666         } catch (Exception e) {
1667             logTroubleScanning(file, e);
1668         }
1669         return op;
1670     }
1671 
populateVideoGeolocationCoordinates( ContentProviderOperation.Builder op, MediaMetadataRetriever mmr)1672     private void populateVideoGeolocationCoordinates(
1673             ContentProviderOperation.Builder op, MediaMetadataRetriever mmr) {
1674         // Extract geolocation data
1675         final int locationArraySize = 2;
1676         // First element of the array is the latitude and the second is the longitude
1677         final int latitudeIndex = 0;
1678         final int longitudeIndex = 1;
1679         String imageGeolocationCoordinates = mmr.extractMetadata(
1680                 MediaMetadataRetriever.METADATA_KEY_LOCATION);
1681         if (imageGeolocationCoordinates != null) {
1682             // The extracted geolocation string is of the form +90.87-87.68.
1683             // where the first half +90.87 is the latitude including the first
1684             // leading +/- sign and the second half -87.68. is the longitude including the
1685             // last '.' character. The following processing includes the signs and
1686             // discards the last '.' character.
1687             String[] locationParts = imageGeolocationCoordinates.split("(?=[+-])");
1688             if (locationParts.length == locationArraySize) {
1689                 float latitude = Float.parseFloat(locationParts[latitudeIndex]);
1690                 // Remove last character which is a '.' in the string
1691                 float longitude = Float.parseFloat(
1692                         locationParts[longitudeIndex].substring(
1693                                 0, locationParts[longitudeIndex].length() - 1));
1694                 op.withValue(VideoColumns.LATITUDE, latitude);
1695                 op.withValue(VideoColumns.LONGITUDE, longitude);
1696             } else {
1697                 Log.e(TAG, "Couldn't extract image geolocation coordinates");
1698             }
1699         }
1700     }
1701 
1702 
1703 
scanItemImage(long existingId, File file, BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName)1704     private @NonNull ContentProviderOperation.Builder scanItemImage(long existingId,
1705             File file, BasicFileAttributes attrs, String mimeType, int mediaType,
1706             String volumeName) {
1707         final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
1708         withGenericValues(op, file, attrs, mimeType, mediaType);
1709 
1710         op.withValue(ImageColumns.DESCRIPTION, null);
1711 
1712         try (FileInputStream is = new FileInputStream(file)) {
1713             final ExifInterface exif = new ExifInterface(is);
1714 
1715             withResolutionValues(op, exif, file);
1716 
1717             withOptionalValue(op, MediaColumns.DATE_TAKEN,
1718                     parseOptionalDateTaken(exif, lastModifiedTime(file, attrs) * 1000));
1719             withOptionalValue(op, MediaColumns.ORIENTATION,
1720                     parseOptionalOrientation(exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
1721                             ExifInterface.ORIENTATION_UNDEFINED)));
1722 
1723             withOptionalValue(op, ImageColumns.DESCRIPTION,
1724                     parseOptional(exif.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION)));
1725             withOptionalValue(op, ImageColumns.EXPOSURE_TIME,
1726                     parseOptional(exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)));
1727             withOptionalValue(op, ImageColumns.F_NUMBER,
1728                     parseOptional(exif.getAttribute(ExifInterface.TAG_F_NUMBER)));
1729             withOptionalValue(op, ImageColumns.ISO,
1730                     parseOptional(exif.getAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS)));
1731             withOptionalValue(op, ImageColumns.SCENE_CAPTURE_TYPE,
1732                     parseOptional(exif.getAttribute(ExifInterface.TAG_SCENE_CAPTURE_TYPE)));
1733 
1734             // TODO b/373352459 Latitude and Longitude for backup and restore
1735             // Fill up the latitude and longitude columns
1736             if (indexMediaLatitudeLongitude()) {
1737                 populateImageGeolocationCoordinates(op, exif);
1738             }
1739 
1740             // Also hunt around for XMP metadata
1741             final XmpInterface xmp = XmpDataParser.createXmpInterface(exif);
1742             withXmpValues(op, xmp, mimeType);
1743 
1744             op.withValue(FileColumns._SPECIAL_FORMAT, SpecialFormatDetector.detect(exif, file));
1745         } catch (Exception e) {
1746             logTroubleScanning(file, e);
1747         }
1748         return op;
1749     }
1750 
populateImageGeolocationCoordinates( ContentProviderOperation.Builder op, ExifInterface exif)1751     private void populateImageGeolocationCoordinates(
1752             ContentProviderOperation.Builder op, ExifInterface exif) {
1753         // Array to hold the geolocation coordinates - latitude and longitude
1754         final int locationArraySize = 2;
1755         float[] locationCoordinates = new float[locationArraySize];
1756         if (exif.getLatLong(locationCoordinates)) {
1757             // First element is the latitude and the second is the longitude
1758             op.withValue(ImageColumns.LATITUDE, locationCoordinates[/* latitudeIndex */ 0]);
1759             op.withValue(ImageColumns.LONGITUDE, locationCoordinates[/* longitudeIndex */ 1]);
1760         }
1761     }
1762 
scanItemFile(long existingId, File file, BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName)1763     private @NonNull ContentProviderOperation.Builder scanItemFile(long existingId,
1764             File file, BasicFileAttributes attrs, String mimeType, int mediaType,
1765             String volumeName) {
1766         final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
1767         withGenericValues(op, file, attrs, mimeType, mediaType);
1768 
1769         return op;
1770     }
1771 
newUpsert( @onNull String volumeName, long existingId)1772     private @NonNull ContentProviderOperation.Builder newUpsert(
1773             @NonNull String volumeName, long existingId) {
1774         final Uri uri = MediaStore.Files.getContentUri(volumeName);
1775         if (existingId == -1) {
1776             return ContentProviderOperation.newInsert(uri)
1777                     .withExceptionAllowed(true);
1778         } else {
1779             return ContentProviderOperation.newUpdate(ContentUris.withAppendedId(uri, existingId))
1780                     .withExpectedCount(1)
1781                     .withExceptionAllowed(true);
1782         }
1783     }
1784 
1785     /**
1786      * Pick the first present {@link Optional} value from the given list.
1787      */
1788     @SafeVarargs
firstPresent(@onNull Optional<T>.... options)1789     private @NonNull <T> Optional<T> firstPresent(@NonNull Optional<T>... options) {
1790         for (Optional<T> option : options) {
1791             if (option.isPresent()) {
1792                 return option;
1793             }
1794         }
1795         return Optional.empty();
1796     }
1797 
1798     @VisibleForTesting
parseOptional(@ullable T value)1799     @NonNull <T> Optional<T> parseOptional(@Nullable T value) {
1800         if (value == null) {
1801             return Optional.empty();
1802         } else if (value instanceof String && ((String) value).length() == 0) {
1803             return Optional.empty();
1804         } else if (value instanceof String && ((String) value).equals("-1")) {
1805             return Optional.empty();
1806         } else if (value instanceof String && ((String) value).trim().length() == 0) {
1807             return Optional.empty();
1808         } else if (value instanceof Number && ((Number) value).intValue() == -1) {
1809             return Optional.empty();
1810         } else {
1811             return Optional.of(value);
1812         }
1813     }
1814 
1815     @VisibleForTesting
parseOptionalOrZero(@ullable T value)1816     @NonNull <T> Optional<T> parseOptionalOrZero(@Nullable T value) {
1817         if (value instanceof String && isZero((String) value)) {
1818             return Optional.empty();
1819         } else if (value instanceof Number && ((Number) value).intValue() == 0) {
1820             return Optional.empty();
1821         } else {
1822             return parseOptional(value);
1823         }
1824     }
1825 
1826     @VisibleForTesting
parseOptionalNumerator(@ullable String value)1827     @NonNull Optional<Integer> parseOptionalNumerator(@Nullable String value) {
1828         final Optional<String> parsedValue = parseOptional(value);
1829         if (parsedValue.isPresent()) {
1830             value = parsedValue.get();
1831             final int fractionIndex = value.indexOf('/');
1832             if (fractionIndex != -1) {
1833                 value = value.substring(0, fractionIndex);
1834             }
1835             try {
1836                 return Optional.of(Integer.parseInt(value));
1837             } catch (NumberFormatException ignored) {
1838                 return Optional.empty();
1839             }
1840         } else {
1841             return Optional.empty();
1842         }
1843     }
1844 
1845     /**
1846      * Try our best to calculate {@link MediaColumns#DATE_TAKEN} in reference to
1847      * the epoch, making our best guess from unrelated fields when offset
1848      * information isn't directly available.
1849      */
1850     @VisibleForTesting
parseOptionalDateTaken(@onNull ExifInterface exif, long lastModifiedTime)1851     @NonNull Optional<Long> parseOptionalDateTaken(@NonNull ExifInterface exif,
1852             long lastModifiedTime) {
1853         final long originalTime = ExifUtils.getDateTimeOriginal(exif);
1854         if (exif.hasAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)) {
1855             // We have known offset information, return it directly!
1856             return Optional.of(originalTime);
1857         } else {
1858             // Otherwise we need to guess the offset from unrelated fields
1859             final long smallestZone = 15 * MINUTE_IN_MILLIS;
1860             final long gpsTime = ExifUtils.getGpsDateTime(exif);
1861             if (gpsTime > 0) {
1862                 final long offset = gpsTime - originalTime;
1863                 if (Math.abs(offset) < 24 * HOUR_IN_MILLIS) {
1864                     final long rounded = Math.round((float) offset / smallestZone) * smallestZone;
1865                     return Optional.of(originalTime + rounded);
1866                 }
1867             }
1868             if (lastModifiedTime > 0) {
1869                 final long offset = lastModifiedTime - originalTime;
1870                 if (Math.abs(offset) < 24 * HOUR_IN_MILLIS) {
1871                     final long rounded = Math.round((float) offset / smallestZone) * smallestZone;
1872                     return Optional.of(originalTime + rounded);
1873                 }
1874             }
1875             return Optional.empty();
1876         }
1877     }
1878 
1879     @VisibleForTesting
parseOptionalOrientation(int orientation)1880     @NonNull Optional<Integer> parseOptionalOrientation(int orientation) {
1881         switch (orientation) {
1882             case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
1883             case ExifInterface.ORIENTATION_NORMAL: return Optional.of(0);
1884             case ExifInterface.ORIENTATION_TRANSPOSE:
1885             case ExifInterface.ORIENTATION_ROTATE_90: return Optional.of(90);
1886             case ExifInterface.ORIENTATION_FLIP_VERTICAL:
1887             case ExifInterface.ORIENTATION_ROTATE_180: return Optional.of(180);
1888             case ExifInterface.ORIENTATION_TRANSVERSE:
1889             case ExifInterface.ORIENTATION_ROTATE_270: return Optional.of(270);
1890             default: return Optional.empty();
1891         }
1892     }
1893 
1894     @VisibleForTesting
parseOptionalVideoResolution(@onNull MediaMetadataRetriever mmr)1895     @NonNull Optional<String> parseOptionalVideoResolution(@NonNull MediaMetadataRetriever mmr) {
1896         final Optional<?> width = parseOptional(mmr.extractMetadata(METADATA_KEY_VIDEO_WIDTH));
1897         final Optional<?> height = parseOptional(mmr.extractMetadata(METADATA_KEY_VIDEO_HEIGHT));
1898         return parseOptionalResolution(width, height);
1899     }
1900 
1901     @VisibleForTesting
parseOptionalImageResolution(@onNull MediaMetadataRetriever mmr)1902     @NonNull Optional<String> parseOptionalImageResolution(@NonNull MediaMetadataRetriever mmr) {
1903         final Optional<?> width = parseOptional(mmr.extractMetadata(METADATA_KEY_IMAGE_WIDTH));
1904         final Optional<?> height = parseOptional(mmr.extractMetadata(METADATA_KEY_IMAGE_HEIGHT));
1905         return parseOptionalResolution(width, height);
1906     }
1907 
1908     @VisibleForTesting
parseOptionalResolution( @onNull ExifInterface exif)1909     @NonNull Optional<String> parseOptionalResolution(
1910             @NonNull ExifInterface exif) {
1911         final Optional<?> width = parseOptionalOrZero(
1912                 exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH));
1913         final Optional<?> height = parseOptionalOrZero(
1914                 exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH));
1915         return parseOptionalResolution(width, height);
1916     }
1917 
parseOptionalResolution( @onNull Optional<?> width, @NonNull Optional<?> height)1918     private @NonNull Optional<String> parseOptionalResolution(
1919             @NonNull Optional<?> width, @NonNull Optional<?> height) {
1920         if (width.isPresent() && height.isPresent()) {
1921             return Optional.of(width.get() + "\u00d7" + height.get());
1922         }
1923         return Optional.empty();
1924     }
1925 
1926     @VisibleForTesting
parseOptionalDate(@ullable String date)1927     @NonNull Optional<Long> parseOptionalDate(@Nullable String date) {
1928         if (TextUtils.isEmpty(date)) return Optional.empty();
1929         try {
1930             synchronized (S_DATE_FORMAT_WITH_MILLIS) {
1931                 return parseDateWithFormat(date, S_DATE_FORMAT_WITH_MILLIS);
1932             }
1933         } catch (ParseException e) {
1934             // Log and try without millis as well
1935             Log.d(TAG, String.format(
1936                     "Parsing date with millis failed for [%s]. We will retry without millis",
1937                     date));
1938         }
1939         try {
1940             synchronized (S_DATE_FORMAT) {
1941                 return parseDateWithFormat(date, S_DATE_FORMAT);
1942             }
1943         } catch (ParseException e) {
1944             Log.d(TAG, String.format("Parsing date without millis failed for [%s]", date));
1945             return Optional.empty();
1946         }
1947     }
1948 
parseDateWithFormat( @ullable String date, SimpleDateFormat dateFormat)1949     private Optional<Long> parseDateWithFormat(
1950             @Nullable String date, SimpleDateFormat dateFormat) throws ParseException {
1951         final long value = dateFormat.parse(date).getTime();
1952         return (value > 0) ? Optional.of(value) : Optional.empty();
1953     }
1954 
1955     @VisibleForTesting
parseOptionalYear(@ullable String value)1956     @NonNull Optional<Integer> parseOptionalYear(@Nullable String value) {
1957         final Optional<String> parsedValue = parseOptional(value);
1958         if (parsedValue.isPresent()) {
1959             final Matcher m = PATTERN_YEAR.matcher(parsedValue.get());
1960             if (m.find()) {
1961                 return Optional.of(Integer.parseInt(m.group(1)));
1962             } else {
1963                 return Optional.empty();
1964             }
1965         } else {
1966             return Optional.empty();
1967         }
1968     }
1969 
1970     @VisibleForTesting
parseOptionalTrack( @onNull MediaMetadataRetriever mmr)1971     @NonNull Optional<Integer> parseOptionalTrack(
1972             @NonNull MediaMetadataRetriever mmr) {
1973         final Optional<Integer> disc = parseOptionalNumerator(
1974                 mmr.extractMetadata(METADATA_KEY_DISC_NUMBER));
1975         final Optional<Integer> track = parseOptionalNumerator(
1976                 mmr.extractMetadata(METADATA_KEY_CD_TRACK_NUMBER));
1977         if (disc.isPresent() && track.isPresent()) {
1978             return Optional.of((disc.get() * 1000) + track.get());
1979         } else {
1980             return track;
1981         }
1982     }
1983 
1984     /**
1985      * Maybe replace the MIME type from extension with the MIME type from the
1986      * refined metadata, but only when the top-level MIME type agrees.
1987      */
1988     @VisibleForTesting
parseOptionalMimeType(@onNull String fileMimeType, @Nullable String refinedMimeType)1989     @NonNull Optional<String> parseOptionalMimeType(@NonNull String fileMimeType,
1990             @Nullable String refinedMimeType) {
1991         // Ignore when missing
1992         if (TextUtils.isEmpty(refinedMimeType)) return Optional.empty();
1993 
1994         // Ignore when invalid
1995         final int refinedSplit = refinedMimeType.indexOf('/');
1996         if (refinedSplit == -1) return Optional.empty();
1997 
1998         if (fileMimeType.regionMatches(true, 0, refinedMimeType, 0, refinedSplit + 1)) {
1999             return Optional.of(refinedMimeType);
2000         } else {
2001             return Optional.empty();
2002         }
2003     }
2004 
2005     /**
2006      * Return last modified time of given file. This value is typically read
2007      * from the given {@link BasicFileAttributes}, except in the case of
2008      * read-only partitions, where {@link Build#TIME} is used instead.
2009      */
lastModifiedTime(@onNull File file, @NonNull BasicFileAttributes attrs)2010     public long lastModifiedTime(@NonNull File file,
2011             @NonNull BasicFileAttributes attrs) {
2012         if (FileUtils.contains(Environment.getStorageDirectory(), file)) {
2013             return attrs.lastModifiedTime().toMillis() / 1000;
2014         } else {
2015             return Build.TIME / 1000;
2016         }
2017     }
2018 
2019     /**
2020      * Test if any parents of given path should be scanned and test if any parents of given
2021      * path should be considered hidden.
2022      */
shouldScanPathAndIsPathHidden(@onNull File dir)2023     Pair<Boolean, Boolean> shouldScanPathAndIsPathHidden(@NonNull File dir) {
2024         Trace.beginSection("Scanner.shouldScanPathAndIsPathHidden");
2025         try {
2026             boolean isPathHidden = false;
2027             while (dir != null) {
2028                 if (!shouldScanDirectory(dir)) {
2029                     // When the path is not scannable, we don't care if it's hidden or not.
2030                     return Pair.create(false, false);
2031                 }
2032                 isPathHidden = isPathHidden || FileUtils.isDirectoryHidden(dir);
2033                 dir = dir.getParentFile();
2034             }
2035             return Pair.create(true, isPathHidden);
2036         } finally {
2037             Trace.endSection();
2038         }
2039     }
2040 
2041     @VisibleForTesting
shouldScanDirectory(@onNull File dir)2042     boolean shouldScanDirectory(@NonNull File dir) {
2043         if (isInARCMyFilesDownloadsDirectory(dir)) {
2044             // In ARC, skip files under MyFiles/Downloads since it's scanned under
2045             // /storage/emulated.
2046             return false;
2047         }
2048 
2049         final File nomedia = new File(dir, ".nomedia");
2050 
2051         // Handle well-known paths that should always be visible or invisible,
2052         // regardless of .nomedia presence
2053         if (FileUtils.shouldBeVisible(dir.getAbsolutePath())) {
2054             // Well known paths can never be a hidden directory. Delete any non-standard nomedia
2055             // presence in well known path.
2056             nomedia.delete();
2057             return true;
2058         }
2059 
2060         if (FileUtils.shouldBeInvisible(dir.getAbsolutePath())) {
2061             // Create the .nomedia file in paths that are not scannable. This is useful when user
2062             // ejects the SD card and brings it to an older device and its media scanner can
2063             // now correctly identify these paths as not scannable.
2064             try {
2065                 nomedia.createNewFile();
2066             } catch (IOException ignored) {
2067             }
2068             return false;
2069         }
2070         return true;
2071     }
2072 
isInARCMyFilesDownloadsDirectory(@onNull File file)2073     private boolean isInARCMyFilesDownloadsDirectory(@NonNull File file) {
2074         return IS_ARC && file.toPath().startsWith(ARC_MYFILES_DOWNLOADS_PATH);
2075     }
2076 
2077     /**
2078      * @return {@link FileColumns#MEDIA_TYPE}, resolved based on the file path and given
2079      * {@code mimeType}.
2080      */
resolveMediaTypeFromFilePath(@onNull File file, @NonNull String mimeType, boolean isHidden)2081     private int resolveMediaTypeFromFilePath(@NonNull File file, @NonNull String mimeType,
2082             boolean isHidden) {
2083         int mediaType = MimeUtils.resolveMediaType(mimeType);
2084 
2085         if (isHidden || FileUtils.isFileHidden(file)) {
2086             mediaType = FileColumns.MEDIA_TYPE_NONE;
2087         }
2088         if (mediaType == FileColumns.MEDIA_TYPE_IMAGE && isFileAlbumArt(file)) {
2089             mediaType = FileColumns.MEDIA_TYPE_NONE;
2090         }
2091         return mediaType;
2092     }
2093 
2094     @VisibleForTesting
isFileAlbumArt(@onNull File file)2095     boolean isFileAlbumArt(@NonNull File file) {
2096         return PATTERN_ALBUM_ART.matcher(file.getName()).matches();
2097     }
2098 
isZero(@onNull String value)2099     boolean isZero(@NonNull String value) {
2100         if (value.length() == 0) {
2101             return false;
2102         }
2103         for (int i = 0; i < value.length(); i++) {
2104             if (value.charAt(i) != '0') {
2105                 return false;
2106             }
2107         }
2108         return true;
2109     }
2110 
logTroubleScanning(@onNull File file, @NonNull Exception e)2111     void logTroubleScanning(@NonNull File file, @NonNull Exception e) {
2112         if (LOGW) Log.w(TAG, "Trouble scanning " + file, e);
2113     }
2114 }
2115