Merge changes from topic "jan7"

* changes:
  Gently ramp up strictness.
  Final push to build against SDK.
diff --git a/Android.bp b/Android.bp
index c0e6459..65996bd 100644
--- a/Android.bp
+++ b/Android.bp
@@ -34,9 +34,7 @@
         "java_api_finder",
     ],
 
-    // STOPSHIP: remove this when building against system_current
-    platform_apis: true,
-    // sdk_version: "system_current",
+    sdk_version: "system_current",
 
     certificate: "media",
     privileged: true,
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 7c3c4f2..69e6c7e 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -162,6 +162,11 @@
     public static final String GET_VERSION_CALL = "get_version";
 
     /** {@hide} */
+    @Deprecated
+    public static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY =
+            "com.android.externalstorage.documents";
+
+    /** {@hide} */
     public static final String GET_DOCUMENT_URI_CALL = "get_document_uri";
     /** {@hide} */
     public static final String GET_MEDIA_URI_CALL = "get_media_uri";
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4309d68..92e98f9 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -27,6 +27,9 @@
     <!-- Description line for music artists in the search/suggestion results -->
     <string name="artist_label">Artist</string>
 
+    <!-- Label for a file or directory which has no valid user-visible description [CHAR LIMIT=32] -->
+    <string name="unknown">Unknown</string>
+
     <!-- Title for documents backend that offers images. [CHAR LIMIT=24] -->
     <string name="root_images">Images</string>
     <!-- Title for documents backend that offers videos. [CHAR LIMIT=24] -->
diff --git a/src/com/android/providers/media/MediaDocumentsProvider.java b/src/com/android/providers/media/MediaDocumentsProvider.java
index 70ca6c0..88daeda 100644
--- a/src/com/android/providers/media/MediaDocumentsProvider.java
+++ b/src/com/android/providers/media/MediaDocumentsProvider.java
@@ -17,11 +17,15 @@
 package com.android.providers.media;
 
 import static android.content.ContentResolver.EXTRA_SIZE;
+import static android.provider.DocumentsContract.QUERY_ARG_DISPLAY_NAME;
+import static android.provider.DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA;
+import static android.provider.DocumentsContract.QUERY_ARG_FILE_SIZE_OVER;
+import static android.provider.DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER;
+import static android.provider.DocumentsContract.QUERY_ARG_MIME_TYPES;
 
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
-import android.content.Intent;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.database.MatrixCursor;
@@ -241,11 +245,6 @@
         });
     }
 
-    static void revokeAllUriGrants(Context context) {
-        context.revokeUriPermission(DocumentsContract.buildBaseDocumentUri(AUTHORITY),
-                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-    }
-
     private static class Ident {
         public String type;
         public long id;
@@ -843,7 +842,7 @@
             Binder.restoreCallingIdentity(token);
         }
 
-        final String[] handledQueryArgs = DocumentsContract.getHandledQueryArguments(queryArgs);
+        final String[] handledQueryArgs = getHandledQueryArguments(queryArgs);
         if (handledQueryArgs.length > 0) {
             final Bundle extras = new Bundle();
             extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs);
@@ -853,6 +852,35 @@
         return result;
     }
 
+    public static String[] getHandledQueryArguments(Bundle queryArgs) {
+        if (queryArgs == null) {
+            return new String[0];
+        }
+
+        final ArrayList<String> args = new ArrayList<>();
+
+        if (queryArgs.keySet().contains(QUERY_ARG_EXCLUDE_MEDIA)) {
+            args.add(QUERY_ARG_EXCLUDE_MEDIA);
+        }
+
+        if (queryArgs.keySet().contains(QUERY_ARG_DISPLAY_NAME)) {
+            args.add(QUERY_ARG_DISPLAY_NAME);
+        }
+
+        if (queryArgs.keySet().contains(QUERY_ARG_FILE_SIZE_OVER)) {
+            args.add(QUERY_ARG_FILE_SIZE_OVER);
+        }
+
+        if (queryArgs.keySet().contains(QUERY_ARG_LAST_MODIFIED_AFTER)) {
+            args.add(QUERY_ARG_LAST_MODIFIED_AFTER);
+        }
+
+        if (queryArgs.keySet().contains(QUERY_ARG_MIME_TYPES)) {
+            args.add(QUERY_ARG_MIME_TYPES);
+        }
+        return args.toArray(new String[0]);
+    }
+
     @Override
     public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
             throws FileNotFoundException {
@@ -1333,13 +1361,13 @@
         if (!MediaStore.UNKNOWN_STRING.equals(displayName)) {
             return displayName;
         }
-        return getContext().getResources().getString(com.android.internal.R.string.unknownName);
+        return getContext().getResources().getString(R.string.unknown);
     }
 
     private String cleanUpMediaBucketName(String bucketDisplayName) {
         if (!TextUtils.isEmpty(bucketDisplayName)) {
             return bucketDisplayName;
         }
-        return getContext().getResources().getString(com.android.internal.R.string.unknownName);
+        return getContext().getResources().getString(R.string.unknown);
     }
 }
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index ca3c24d..c6e19d8 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -66,6 +66,7 @@
 
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OnOpActiveChangedListener;
+import android.app.DownloadManager;
 import android.app.PendingIntent;
 import android.app.RecoverableSecurityException;
 import android.app.RemoteAction;
@@ -93,7 +94,6 @@
 import android.content.res.AssetFileDescriptor;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.database.AbstractCursor;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.sqlite.SQLiteDatabase;
@@ -115,20 +115,17 @@
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFileDescriptor.OnCloseListener;
-import android.os.RedactingFileDescriptor;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
-import android.os.storage.StorageEventListener;
 import android.os.storage.StorageManager;
+import android.os.storage.StorageManager.StorageVolumeCallback;
 import android.os.storage.StorageVolume;
-import android.os.storage.VolumeInfo;
 import android.preference.PreferenceManager;
 import android.provider.BaseColumns;
 import android.provider.Column;
-import android.provider.DocumentsContract;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio;
 import android.provider.MediaStore.Audio.AudioColumns;
@@ -174,6 +171,7 @@
 import com.android.providers.media.util.LongArray;
 import com.android.providers.media.util.Metrics;
 import com.android.providers.media.util.MimeUtils;
+import com.android.providers.media.util.RedactingFileDescriptor;
 import com.android.providers.media.util.XmpInterface;
 
 import com.google.common.hash.Hashing;
@@ -610,17 +608,13 @@
         context.registerReceiver(mMediaReceiver, filter);
 
         // Watch for invalidation of cached volumes
-        mStorageManager.registerListener(new StorageEventListener() {
-            @Override
-            public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
-                updateVolumes();
-            }
-
-            @Override
-            public void onStorageStateChanged(String path, String oldState, String newState) {
-                updateVolumes();
-            }
-        });
+        mStorageManager.registerStorageVolumeCallback(context.getMainExecutor(),
+                new StorageVolumeCallback() {
+                    @Override
+                    public void onStateChanged(@NonNull StorageVolume volume) {
+                        updateVolumes();
+                    }
+                });
 
         updateVolumes();
         attachVolume(MediaStore.VOLUME_INTERNAL);
@@ -1132,7 +1126,6 @@
         }
     }
 
-
     @Override
     public int checkUriPermission(@NonNull Uri uri, int uid,
             /* @Intent.AccessUriMode */ int modeFlags) {
@@ -1323,8 +1316,9 @@
                     }
 
                     final MatrixCursor cursor = new MatrixCursor(projection);
-                    final String data = ContentResolver.translateDeprecatedDataPath(
+                    final File file = ContentResolver.encodeToFile(
                             fullUri.buildUpon().appendPath("thumbnail").build());
+                    final String data = file.getAbsolutePath();
                     cursor.newRow().add(MediaColumns._ID, null)
                             .add(Images.Thumbnails.IMAGE_ID, id)
                             .add(Video.Thumbnails.VIDEO_ID, id)
@@ -1345,8 +1339,11 @@
                 selection, selectionArgs, groupBy, having, sortOrder, limit, signal);
 
         if (c != null) {
-            ((AbstractCursor) c).setNotificationUris(getContext().getContentResolver(),
-                    Arrays.asList(uri), UserHandle.myUserId(), false);
+            // As a performance optimization, only configure notifications when
+            // resulting cursor will leave our process
+            if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
+                c.setNotificationUri(getContext().getContentResolver(), uri);
+            }
 
             final Bundle extras = new Bundle();
             extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS,
@@ -2542,11 +2539,12 @@
         if (parseBoolean(uri.getQueryParameter("distinct"))) {
             qb.setDistinct(true);
         }
-        qb.setProjectionAggregationAllowed(true);
         qb.setStrict(true);
         // TODO: re-enable as part of fixing b/146518586
-        // qb.setStrictColumns(true);
-        // qb.setStrictGrammar(true);
+        if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
+            qb.setStrictColumns(true);
+            qb.setStrictGrammar(true);
+        }
 
         final String callingPackage = getCallingPackageOrSelf();
 
@@ -3268,23 +3266,9 @@
 
             if (deletedDownloadIds.size() > 0) {
                 final long token = Binder.clearCallingIdentity();
-                try (ContentProviderClient client = getContext().getContentResolver()
-                     .acquireUnstableContentProviderClient(
-                             android.provider.Downloads.Impl.AUTHORITY)) {
-                    final Bundle callExtras = new Bundle();
-                    final long[] ids = new long[deletedDownloadIds.size()];
-                    final String[] mimeTypes = new String[deletedDownloadIds.size()];
-                    for (int i = deletedDownloadIds.size() - 1; i >= 0; --i) {
-                        ids[i] = deletedDownloadIds.keyAt(i);
-                        mimeTypes[i] = deletedDownloadIds.valueAt(i);
-                    }
-                    callExtras.putLongArray(android.provider.Downloads.EXTRA_IDS, ids);
-                    callExtras.putStringArray(android.provider.Downloads.EXTRA_MIME_TYPES,
-                            mimeTypes);
-                    client.call(android.provider.Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED,
-                            null, callExtras);
-                } catch (RemoteException e) {
-                    // Should not happen
+                try {
+                    getContext().getSystemService(DownloadManager.class)
+                            .onMediaStoreDownloadsDeleted(deletedDownloadIds);
                 } finally {
                     Binder.restoreCallingIdentity(token);
                 }
@@ -3397,7 +3381,7 @@
                 return res;
             }
             case MediaStore.GET_DOCUMENT_URI_CALL: {
-                final Uri mediaUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
+                final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI);
                 enforceCallingPermission(mediaUri, extras, false);
 
                 final Uri fileUri;
@@ -3412,24 +3396,24 @@
 
                 try (ContentProviderClient client = getContext().getContentResolver()
                         .acquireUnstableContentProviderClient(
-                                DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
-                    extras.putParcelable(DocumentsContract.EXTRA_URI, fileUri);
+                                MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
+                    extras.putParcelable(MediaStore.EXTRA_URI, fileUri);
                     return client.call(method, null, extras);
                 } catch (RemoteException e) {
                     throw new IllegalStateException(e);
                 }
             }
             case MediaStore.GET_MEDIA_URI_CALL: {
-                final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
+                final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI);
                 getContext().enforceCallingUriPermission(documentUri,
                         Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG);
 
                 final Uri fileUri;
                 try (ContentProviderClient client = getContext().getContentResolver()
                         .acquireUnstableContentProviderClient(
-                                DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
+                                MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
                     final Bundle res = client.call(method, null, extras);
-                    fileUri = res.getParcelable(DocumentsContract.EXTRA_URI);
+                    fileUri = res.getParcelable(MediaStore.EXTRA_URI);
                 } catch (RemoteException e) {
                     throw new IllegalStateException(e);
                 }
@@ -3437,7 +3421,7 @@
                 final LocalCallingIdentity token = clearLocalCallingIdentity();
                 try {
                     final Bundle res = new Bundle();
-                    res.putParcelable(DocumentsContract.EXTRA_URI,
+                    res.putParcelable(MediaStore.EXTRA_URI,
                             queryForMediaUri(new File(fileUri.getPath()), null));
                     return res;
                 } catch (FileNotFoundException e) {
@@ -4555,7 +4539,7 @@
 
             // Second, wrap in any listener that we've requested
             if (!isPending && forWrite && listener != null) {
-                return ParcelFileDescriptor.fromPfd(pfd, BackgroundThread.getHandler(), listener);
+                return ParcelFileDescriptor.wrap(pfd, BackgroundThread.getHandler(), listener);
             } else {
                 return pfd;
             }
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index ab8deb1..7595d88 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -86,6 +86,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.providers.media.util.ExifUtils;
 import com.android.providers.media.util.FileUtils;
 import com.android.providers.media.util.IsoInterface;
 import com.android.providers.media.util.Logging;
@@ -972,14 +973,14 @@
      */
     static @NonNull Optional<Long> parseOptionalDateTaken(@NonNull ExifInterface exif,
             long lastModifiedTime) {
-        final long originalTime = exif.getDateTimeOriginal();
+        final long originalTime = ExifUtils.getDateTimeOriginal(exif);
         if (exif.hasAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)) {
             // We have known offset information, return it directly!
             return Optional.of(originalTime);
         } else {
             // Otherwise we need to guess the offset from unrelated fields
             final long smallestZone = 15 * MINUTE_IN_MILLIS;
-            final long gpsTime = exif.getGpsDateTime();
+            final long gpsTime = ExifUtils.getGpsDateTime(exif);
             if (gpsTime > 0) {
                 final long offset = gpsTime - originalTime;
                 if (Math.abs(offset) < 24 * HOUR_IN_MILLIS) {
diff --git a/src/com/android/providers/media/util/ExifUtils.java b/src/com/android/providers/media/util/ExifUtils.java
new file mode 100644
index 0000000..09674a8
--- /dev/null
+++ b/src/com/android/providers/media/util/ExifUtils.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+import static android.media.ExifInterface.TAG_DATETIME;
+import static android.media.ExifInterface.TAG_DATETIME_DIGITIZED;
+import static android.media.ExifInterface.TAG_DATETIME_ORIGINAL;
+import static android.media.ExifInterface.TAG_GPS_DATESTAMP;
+import static android.media.ExifInterface.TAG_GPS_TIMESTAMP;
+import static android.media.ExifInterface.TAG_OFFSET_TIME;
+import static android.media.ExifInterface.TAG_OFFSET_TIME_DIGITIZED;
+import static android.media.ExifInterface.TAG_OFFSET_TIME_ORIGINAL;
+import static android.media.ExifInterface.TAG_SUBSEC_TIME;
+import static android.media.ExifInterface.TAG_SUBSEC_TIME_DIGITIZED;
+import static android.media.ExifInterface.TAG_SUBSEC_TIME_ORIGINAL;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.Nullable;
+import android.media.ExifInterface;
+
+import androidx.annotation.NonNull;
+
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+
+/**
+ * Utility methods borrowed from {@link ExifInterface} since they're not
+ * official APIs yet.
+ */
+public class ExifUtils {
+    // Pattern to check non zero timestamp
+    private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
+
+    private static final SimpleDateFormat sFormatter;
+    private static final SimpleDateFormat sFormatterTz;
+
+    static {
+        sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
+        sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+        sFormatterTz = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss XXX");
+        sFormatterTz.setTimeZone(TimeZone.getTimeZone("UTC"));
+    }
+
+    /**
+     * Returns parsed {@code DateTime} value, or -1 if unavailable or invalid.
+     */
+    public static @CurrentTimeMillisLong long getDateTime(@NonNull ExifInterface exif) {
+        return parseDateTime(exif.getAttribute(TAG_DATETIME),
+                exif.getAttribute(TAG_SUBSEC_TIME),
+                exif.getAttribute(TAG_OFFSET_TIME));
+    }
+
+    /**
+     * Returns parsed {@code DateTimeDigitized} value, or -1 if unavailable or
+     * invalid.
+     */
+    public static @CurrentTimeMillisLong long getDateTimeDigitized(@NonNull ExifInterface exif) {
+        return parseDateTime(exif.getAttribute(TAG_DATETIME_DIGITIZED),
+                exif.getAttribute(TAG_SUBSEC_TIME_DIGITIZED),
+                exif.getAttribute(TAG_OFFSET_TIME_DIGITIZED));
+    }
+
+    /**
+     * Returns parsed {@code DateTimeOriginal} value, or -1 if unavailable or
+     * invalid.
+     */
+    public static @CurrentTimeMillisLong long getDateTimeOriginal(@NonNull ExifInterface exif) {
+        return parseDateTime(exif.getAttribute(TAG_DATETIME_ORIGINAL),
+                exif.getAttribute(TAG_SUBSEC_TIME_ORIGINAL),
+                exif.getAttribute(TAG_OFFSET_TIME_ORIGINAL));
+    }
+
+    /**
+     * Returns parsed {@code GPSDateStamp} value, or -1 if unavailable or
+     * invalid.
+     */
+    public static long getGpsDateTime(ExifInterface exif) {
+        String date = exif.getAttribute(TAG_GPS_DATESTAMP);
+        String time = exif.getAttribute(TAG_GPS_TIMESTAMP);
+        if (date == null || time == null
+                || (!sNonZeroTimePattern.matcher(date).matches()
+                && !sNonZeroTimePattern.matcher(time).matches())) {
+            return -1;
+        }
+
+        String dateTimeString = date + ' ' + time;
+
+        ParsePosition pos = new ParsePosition(0);
+        try {
+            Date datetime = sFormatter.parse(dateTimeString, pos);
+            if (datetime == null) return -1;
+            return datetime.getTime();
+        } catch (IllegalArgumentException e) {
+            return -1;
+        }
+    }
+
+    private static @CurrentTimeMillisLong long parseDateTime(@Nullable String dateTimeString,
+            @Nullable String subSecs, @Nullable String offsetString) {
+        if (dateTimeString == null
+                || !sNonZeroTimePattern.matcher(dateTimeString).matches()) return -1;
+
+        ParsePosition pos = new ParsePosition(0);
+        try {
+            // The exif field is in local time. Parsing it as if it is UTC will yield time
+            // since 1/1/1970 local time
+            Date datetime = sFormatter.parse(dateTimeString, pos);
+
+            if (offsetString != null) {
+                dateTimeString = dateTimeString + " " + offsetString;
+                ParsePosition position = new ParsePosition(0);
+                datetime = sFormatterTz.parse(dateTimeString, position);
+            }
+
+            if (datetime == null) return -1;
+            long msecs = datetime.getTime();
+
+            if (subSecs != null) {
+                try {
+                    long sub = Long.parseLong(subSecs);
+                    while (sub > 1000) {
+                        sub /= 10;
+                    }
+                    msecs += sub;
+                } catch (NumberFormatException e) {
+                    // Ignored
+                }
+            }
+            return msecs;
+        } catch (IllegalArgumentException e) {
+            return -1;
+        }
+    }
+}
diff --git a/src/com/android/providers/media/util/RedactingFileDescriptor.java b/src/com/android/providers/media/util/RedactingFileDescriptor.java
new file mode 100644
index 0000000..09aef01
--- /dev/null
+++ b/src/com/android/providers/media/util/RedactingFileDescriptor.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+import static android.os.ParcelFileDescriptor.MODE_APPEND;
+import static android.os.ParcelFileDescriptor.MODE_CREATE;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.os.ParcelFileDescriptor.MODE_TRUNCATE;
+import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;
+import static android.system.OsConstants.F_OK;
+import static android.system.OsConstants.O_ACCMODE;
+import static android.system.OsConstants.O_APPEND;
+import static android.system.OsConstants.O_CREAT;
+import static android.system.OsConstants.O_RDONLY;
+import static android.system.OsConstants.O_RDWR;
+import static android.system.OsConstants.O_TRUNC;
+import static android.system.OsConstants.O_WRONLY;
+import static android.system.OsConstants.R_OK;
+import static android.system.OsConstants.W_OK;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.ProxyFileDescriptorCallback;
+import android.os.storage.StorageManager;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.Arrays;
+
+/**
+ * Variant of {@link FileDescriptor} that allows its creator to specify regions
+ * that should be redacted.
+ *
+ * @deprecated will no longer be needed once FUSE is enabled
+ */
+@Deprecated
+public class RedactingFileDescriptor {
+    private static final String TAG = "RedactingFileDescriptor";
+    private static final boolean DEBUG = true;
+
+    private volatile long[] mRedactRanges;
+    private volatile long[] mFreeOffsets;
+
+    private FileDescriptor mInner = null;
+    private ParcelFileDescriptor mOuter = null;
+
+    public static void closeQuietly(FileDescriptor fd) {
+        try {
+            Os.close(fd);
+        } catch (ErrnoException ignored) {
+        }
+    }
+
+    /** {@hide} */
+    public static int translateModeStringToPosix(String mode) {
+        // Sanity check for invalid chars
+        for (int i = 0; i < mode.length(); i++) {
+            switch (mode.charAt(i)) {
+                case 'r':
+                case 'w':
+                case 't':
+                case 'a':
+                    break;
+                default:
+                    throw new IllegalArgumentException("Bad mode: " + mode);
+            }
+        }
+
+        int res = 0;
+        if (mode.startsWith("rw")) {
+            res = O_RDWR | O_CREAT;
+        } else if (mode.startsWith("w")) {
+            res = O_WRONLY | O_CREAT;
+        } else if (mode.startsWith("r")) {
+            res = O_RDONLY;
+        } else {
+            throw new IllegalArgumentException("Bad mode: " + mode);
+        }
+        if (mode.indexOf('t') != -1) {
+            res |= O_TRUNC;
+        }
+        if (mode.indexOf('a') != -1) {
+            res |= O_APPEND;
+        }
+        return res;
+    }
+
+    /** {@hide} */
+    public static String translateModePosixToString(int mode) {
+        String res = "";
+        if ((mode & O_ACCMODE) == O_RDWR) {
+            res = "rw";
+        } else if ((mode & O_ACCMODE) == O_WRONLY) {
+            res = "w";
+        } else if ((mode & O_ACCMODE) == O_RDONLY) {
+            res = "r";
+        } else {
+            throw new IllegalArgumentException("Bad mode: " + mode);
+        }
+        if ((mode & O_TRUNC) == O_TRUNC) {
+            res += "t";
+        }
+        if ((mode & O_APPEND) == O_APPEND) {
+            res += "a";
+        }
+        return res;
+    }
+
+    /** {@hide} */
+    public static int translateModePosixToPfd(int mode) {
+        int res = 0;
+        if ((mode & O_ACCMODE) == O_RDWR) {
+            res = MODE_READ_WRITE;
+        } else if ((mode & O_ACCMODE) == O_WRONLY) {
+            res = MODE_WRITE_ONLY;
+        } else if ((mode & O_ACCMODE) == O_RDONLY) {
+            res = MODE_READ_ONLY;
+        } else {
+            throw new IllegalArgumentException("Bad mode: " + mode);
+        }
+        if ((mode & O_CREAT) == O_CREAT) {
+            res |= MODE_CREATE;
+        }
+        if ((mode & O_TRUNC) == O_TRUNC) {
+            res |= MODE_TRUNCATE;
+        }
+        if ((mode & O_APPEND) == O_APPEND) {
+            res |= MODE_APPEND;
+        }
+        return res;
+    }
+
+    /** {@hide} */
+    public static int translateModePfdToPosix(int mode) {
+        int res = 0;
+        if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) {
+            res = O_RDWR;
+        } else if ((mode & MODE_WRITE_ONLY) == MODE_WRITE_ONLY) {
+            res = O_WRONLY;
+        } else if ((mode & MODE_READ_ONLY) == MODE_READ_ONLY) {
+            res = O_RDONLY;
+        } else {
+            throw new IllegalArgumentException("Bad mode: " + mode);
+        }
+        if ((mode & MODE_CREATE) == MODE_CREATE) {
+            res |= O_CREAT;
+        }
+        if ((mode & MODE_TRUNCATE) == MODE_TRUNCATE) {
+            res |= O_TRUNC;
+        }
+        if ((mode & MODE_APPEND) == MODE_APPEND) {
+            res |= O_APPEND;
+        }
+        return res;
+    }
+
+    /** {@hide} */
+    public static int translateModeAccessToPosix(int mode) {
+        if (mode == F_OK) {
+            // There's not an exact mapping, so we attempt a read-only open to
+            // determine if a file exists
+            return O_RDONLY;
+        } else if ((mode & (R_OK | W_OK)) == (R_OK | W_OK)) {
+            return O_RDWR;
+        } else if ((mode & R_OK) == R_OK) {
+            return O_RDONLY;
+        } else if ((mode & W_OK) == W_OK) {
+            return O_WRONLY;
+        } else {
+            throw new IllegalArgumentException("Bad mode: " + mode);
+        }
+    }
+
+    private RedactingFileDescriptor(
+            Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)
+            throws IOException {
+        mRedactRanges = checkRangesArgument(redactRanges);
+        mFreeOffsets = freeOffsets;
+
+        try {
+            try {
+                mInner = Os.open(file.getAbsolutePath(),
+                        translateModePfdToPosix(mode), 0);
+                mOuter = context.getSystemService(StorageManager.class)
+                        .openProxyFileDescriptor(mode, mCallback,
+                                new Handler(Looper.getMainLooper()));
+            } catch (ErrnoException e) {
+                throw e.rethrowAsIOException();
+            }
+        } catch (IOException e) {
+            closeQuietly(mInner);
+            FileUtils.closeQuietly(mOuter);
+            throw e;
+        }
+    }
+
+    private static long[] checkRangesArgument(long[] ranges) {
+        if (ranges.length % 2 != 0) {
+            throw new IllegalArgumentException();
+        }
+        for (int i = 0; i < ranges.length - 1; i += 2) {
+            if (ranges[i] > ranges[i + 1]) {
+                throw new IllegalArgumentException();
+            }
+        }
+        return ranges;
+    }
+
+    /**
+     * Open the given {@link File} and returns a {@link ParcelFileDescriptor}
+     * that offers a redacted view of the underlying data. If a redacted region
+     * is written to, the newly written data can be read back correctly instead
+     * of continuing to be redacted.
+     *
+     * @param file The underlying file to open.
+     * @param mode The {@link ParcelFileDescriptor} mode to open with.
+     * @param redactRanges List of file ranges that should be redacted, stored
+     *            as {@code [start1, end1, start2, end2, ...]}. Start values are
+     *            inclusive and end values are exclusive.
+     * @param freePositions List of file offsets at which the four byte value 'free' should be
+     *            written instead of zeros within parts of the file covered by {@code redactRanges}.
+     *            Non-redacted bytes will not be modified even if covered by a 'free'. This is
+     *            useful for overwriting boxes in ISOBMFF files with padding data.
+     */
+    public static ParcelFileDescriptor open(Context context, File file, int mode,
+            long[] redactRanges, long[] freePositions) throws IOException {
+        return new RedactingFileDescriptor(context, file, mode, redactRanges, freePositions).mOuter;
+    }
+
+    /**
+     * Update the given ranges argument to remove any references to the given
+     * offset and length. This is typically used when a caller has written over
+     * a previously redacted region.
+     */
+    public static long[] removeRange(long[] ranges, long start, long end) {
+        if (start == end) {
+            return ranges;
+        } else if (start > end) {
+            throw new IllegalArgumentException();
+        }
+
+        long[] res = new long[0];
+        for (int i = 0; i < ranges.length; i += 2) {
+            if (start <= ranges[i] && end >= ranges[i + 1]) {
+                // Range entirely covered; remove it
+            } else if (start >= ranges[i] && end <= ranges[i + 1]) {
+                // Range partially covered; punch a hole
+                res = Arrays.copyOf(res, res.length + 4);
+                res[res.length - 4] = ranges[i];
+                res[res.length - 3] = start;
+                res[res.length - 2] = end;
+                res[res.length - 1] = ranges[i + 1];
+            } else {
+                // Range might covered; adjust edges if needed
+                res = Arrays.copyOf(res, res.length + 2);
+                if (end >= ranges[i] && end <= ranges[i + 1]) {
+                    res[res.length - 2] = Math.max(ranges[i], end);
+                } else {
+                    res[res.length - 2] = ranges[i];
+                }
+                if (start >= ranges[i] && start <= ranges[i + 1]) {
+                    res[res.length - 1] = Math.min(ranges[i + 1], start);
+                } else {
+                    res[res.length - 1] = ranges[i + 1];
+                }
+            }
+        }
+        return res;
+    }
+
+    private final ProxyFileDescriptorCallback mCallback = new ProxyFileDescriptorCallback() {
+        @Override
+        public long onGetSize() throws ErrnoException {
+            return Os.fstat(mInner).st_size;
+        }
+
+        @Override
+        public int onRead(long offset, int size, byte[] data) throws ErrnoException {
+            int n = 0;
+            while (n < size) {
+                try {
+                    final int res = Os.pread(mInner, data, n, size - n, offset + n);
+                    if (res == 0) {
+                        break;
+                    } else {
+                        n += res;
+                    }
+                } catch (InterruptedIOException e) {
+                    n += e.bytesTransferred;
+                }
+            }
+
+            // Redact any relevant ranges before returning
+            final long[] ranges = mRedactRanges;
+            for (int i = 0; i < ranges.length; i += 2) {
+                final long start = Math.max(offset, ranges[i]);
+                final long end = Math.min(offset + size, ranges[i + 1]);
+                for (long j = start; j < end; j++) {
+                    data[(int) (j - offset)] = 0;
+                }
+                // Overwrite data at 'free' offsets within the redaction ranges.
+                for (long freeOffset : mFreeOffsets) {
+                    final long freeEnd = freeOffset + 4;
+                    final long redactFreeStart = Math.max(freeOffset, start);
+                    final long redactFreeEnd = Math.min(freeEnd, end);
+                    for (long j = redactFreeStart; j < redactFreeEnd; j++) {
+                        data[(int) (j - offset)] = (byte) "free".charAt((int) (j - freeOffset));
+                    }
+                }
+            }
+            return n;
+        }
+
+        @Override
+        public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
+            int n = 0;
+            while (n < size) {
+                try {
+                    final int res = Os.pwrite(mInner, data, n, size - n, offset + n);
+                    if (res == 0) {
+                        break;
+                    } else {
+                        n += res;
+                    }
+                } catch (InterruptedIOException e) {
+                    n += e.bytesTransferred;
+                }
+            }
+
+            // Clear any relevant redaction ranges before returning, since the
+            // writer should have access to see the data they just overwrote
+            mRedactRanges = removeRange(mRedactRanges, offset, offset + n);
+            return n;
+        }
+
+        @Override
+        public void onFsync() throws ErrnoException {
+            Os.fsync(mInner);
+        }
+
+        @Override
+        public void onRelease() {
+            closeQuietly(mInner);
+        }
+    };
+}
diff --git a/tests/src/com/android/providers/media/util/RedactingFileDescriptorTest.java b/tests/src/com/android/providers/media/util/RedactingFileDescriptorTest.java
new file mode 100644
index 0000000..6ae807b
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/RedactingFileDescriptorTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.os.RedactingFileDescriptor.removeRange;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.system.Os;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.Arrays;
+
+/**
+ * @deprecated will no longer be needed once FUSE is enabled
+ */
+@Deprecated
+@RunWith(AndroidJUnit4.class)
+public class RedactingFileDescriptorTest {
+    private Context mContext;
+    private File mFile;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mFile = File.createTempFile("redacting", "dat");
+        try (FileOutputStream out = new FileOutputStream(mFile)) {
+            final byte[] buf = new byte[1_000_000];
+            Arrays.fill(buf, (byte) 64);
+            out.write(buf);
+        }
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mFile.delete();
+    }
+
+    @Test
+    public void testSingleByte() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_ONLY,
+                new long[] { 10, 11 }, new long[] {}).getFileDescriptor();
+
+        final byte[] buf = new byte[1_000];
+        assertEquals(buf.length, Os.read(fd, buf, 0, buf.length));
+        for (int i = 0; i < buf.length; i++) {
+            if (i == 10) {
+                assertEquals(0, buf[i]);
+            } else {
+                assertEquals(64, buf[i]);
+            }
+        }
+    }
+
+    @Test
+    public void testRanges() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_ONLY,
+                new long[] { 100, 200, 300, 400 }, new long[] {}).getFileDescriptor();
+
+        final byte[] buf = new byte[10];
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 90));
+        assertArrayEquals(new byte[] { 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 }, buf);
+
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 95));
+        assertArrayEquals(new byte[] { 64, 64, 64, 64, 64, 0, 0, 0, 0, 0 }, buf);
+
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 100));
+        assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, buf);
+
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 195));
+        assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 64, 64, 64, 64, 64 }, buf);
+
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 395));
+        assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 64, 64, 64, 64, 64 }, buf);
+    }
+
+    @Test
+    public void testEntireFile() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_ONLY,
+                new long[] { 0, 5_000_000 }, new long[] {}).getFileDescriptor();
+
+        try (FileInputStream in = new FileInputStream(fd)) {
+            int val;
+            while ((val = in.read()) != -1) {
+                assertEquals(0, val);
+            }
+        }
+    }
+
+    @Test
+    public void testReadWrite() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_WRITE,
+                new long[] { 100, 200, 300, 400 }, new long[] {}).getFileDescriptor();
+
+        // Redacted at first
+        final byte[] buf = new byte[10];
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 95));
+        assertArrayEquals(new byte[] { 64, 64, 64, 64, 64, 0, 0, 0, 0, 0 }, buf);
+
+        // But we can see data that we've written
+        Os.pwrite(fd, new byte[] { 32, 32 }, 0, 2, 102);
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 95));
+        assertArrayEquals(new byte[] { 64, 64, 64, 64, 64, 0, 0, 32, 32, 0 }, buf);
+    }
+
+    @Test
+    public void testRemoveRange() throws Exception {
+        // Removing outside ranges should have no changes
+        assertArrayEquals(new long[] { 100, 200, 300, 400 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 0, 100));
+        assertArrayEquals(new long[] { 100, 200, 300, 400 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 200, 300));
+        assertArrayEquals(new long[] { 100, 200, 300, 400 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 400, 500));
+
+        // Removing full regions
+        assertArrayEquals(new long[] { 100, 200 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 300, 400));
+        assertArrayEquals(new long[] { 100, 200 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 250, 450));
+        assertArrayEquals(new long[] { 300, 400 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 50, 250));
+        assertArrayEquals(new long[] { },
+                removeRange(new long[] { 100, 200, 300, 400 }, 0, 5_000_000));
+    }
+
+    @Test
+    public void testRemoveRange_Partial() throws Exception {
+        assertArrayEquals(new long[] { 150, 200, 300, 400 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 50, 150));
+        assertArrayEquals(new long[] { 100, 150, 300, 400 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 150, 250));
+        assertArrayEquals(new long[] { 100, 150, 350, 400 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 150, 350));
+        assertArrayEquals(new long[] { 100, 150 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 150, 500));
+    }
+
+    @Test
+    public void testRemoveRange_Hole() throws Exception {
+        assertArrayEquals(new long[] { 100, 125, 175, 200, 300, 400 },
+                removeRange(new long[] { 100, 200, 300, 400 }, 125, 175));
+        assertArrayEquals(new long[] { 100, 200 },
+                removeRange(new long[] { 100, 200 }, 150, 150));
+    }
+
+    @Test
+    public void testFreeAtStart() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_WRITE,
+                new long[] { 1, 10 }, new long[] {1}).getFileDescriptor();
+
+        final byte[] buf = new byte[10];
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 0));
+        assertArrayEquals(
+                new byte[] { 64, (byte) 'f', (byte) 'r', (byte) 'e', (byte) 'e', 0, 0, 0, 0, 0 },
+                buf);
+    }
+
+    @Test
+    public void testFreeAtOffset() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_WRITE,
+                new long[] { 1, 10 }, new long[] {3}).getFileDescriptor();
+
+        final byte[] buf = new byte[10];
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 0));
+        assertArrayEquals(
+                new byte[] { 64, 0, 0, (byte) 'f', (byte) 'r', (byte) 'e', (byte) 'e', 0, 0, 0 },
+                buf);
+    }
+
+    @Test
+    public void testFreeAcrossRedactionStart() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_WRITE,
+                new long[] { 1, 10 }, new long[] {0}).getFileDescriptor();
+
+        final byte[] buf = new byte[10];
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 0));
+        assertArrayEquals(
+                new byte[] { 64, (byte) 'r', (byte) 'e', (byte) 'e', 0, 0, 0, 0, 0, 0 },
+                buf);
+    }
+
+    @Test
+    public void testFreeAcrossRedactionEnd() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_WRITE,
+                new long[] { 1, 3 }, new long[] {2}).getFileDescriptor();
+
+        final byte[] buf = new byte[10];
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 0));
+        assertArrayEquals(
+                new byte[] { 64, 0, (byte) 'f', 64, 64, 64, 64, 64, 64, 64 },
+                buf);
+    }
+
+    @Test
+    public void testFreeOutsideRedaction() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_WRITE,
+                new long[] { 1, 8 }, new long[] { 8 }).getFileDescriptor();
+
+        final byte[] buf = new byte[10];
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 0));
+        assertArrayEquals(
+                new byte[] { 64, 0, 0, 0, 0, 0, 0, 0, 64, 64 },
+                buf);
+    }
+
+    @Test
+    public void testFreeMultipleRedactions() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_WRITE,
+                new long[] { 1, 2, 3, 4 }, new long[] { 0 }).getFileDescriptor();
+
+        final byte[] buf = new byte[10];
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 0));
+        assertArrayEquals(
+                new byte[] { 64, (byte) 'r', 64, (byte) 'e', 64, 64, 64, 64, 64, 64 },
+                buf);
+    }
+}