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);
+ }
+}