diff options
| -rw-r--r-- | api/current.txt | 1 | ||||
| -rw-r--r-- | cmds/content/src/com/android/commands/content/Content.java | 5 | ||||
| -rw-r--r-- | core/java/android/os/ParcelFileDescriptor.java | 18 | ||||
| -rw-r--r-- | core/java/android/os/RedactingFileDescriptor.java | 86 | ||||
| -rw-r--r-- | core/java/android/provider/MediaStore.java | 20 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java | 73 |
6 files changed, 178 insertions, 25 deletions
diff --git a/api/current.txt b/api/current.txt index 3b9733f68aad..3967896d76fe 100644 --- a/api/current.txt +++ b/api/current.txt @@ -37297,6 +37297,7 @@ package android.provider { method public static java.lang.String getVolumeName(android.net.Uri); method public static android.provider.MediaStore.PendingSession openPending(android.content.Context, android.net.Uri); method public static android.net.Uri setIncludePending(android.net.Uri); + method public static android.net.Uri setRequireOriginal(android.net.Uri); field public static final java.lang.String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE"; field public static final java.lang.String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE"; field public static final java.lang.String ACTION_REVIEW = "android.provider.action.REVIEW"; diff --git a/cmds/content/src/com/android/commands/content/Content.java b/cmds/content/src/com/android/commands/content/Content.java index 1597c8c9c2b2..52a2ab407f91 100644 --- a/cmds/content/src/com/android/commands/content/Content.java +++ b/cmds/content/src/com/android/commands/content/Content.java @@ -77,7 +77,7 @@ public class Content { + " <BINDING> binds a typed value to a column and is formatted:\n" + " <COLUMN_NAME>:<TYPE>:<COLUMN_VALUE> where:\n" + " <TYPE> specifies data type such as:\n" - + " b - boolean, s - string, i - integer, l - long, f - float, d - double\n" + + " b - boolean, s - string, i - integer, l - long, f - float, d - double, n - null\n" + " Note: Omit the value for passing an empty string, e.g column:s:\n" + " Example:\n" + " # Add \"new_setting\" secure setting with value \"new_value\".\n" @@ -153,6 +153,7 @@ public class Content { private static final String TYPE_LONG = "l"; private static final String TYPE_FLOAT = "f"; private static final String TYPE_DOUBLE = "d"; + private static final String TYPE_NULL = "n"; private static final String COLON = ":"; private static final String ARGUMENT_PREFIX = "--"; @@ -410,6 +411,8 @@ public class Content { values.put(column, Long.parseLong(value)); } else if (TYPE_FLOAT.equalsIgnoreCase(type) || TYPE_DOUBLE.equalsIgnoreCase(type)) { values.put(column, Double.parseDouble(value)); + } else if (TYPE_NULL.equalsIgnoreCase(type)) { + values.putNull(column); } else { throw new IllegalArgumentException("Unsupported type: " + type); } diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 44b9e311dc0b..6de1ff4bc097 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -17,12 +17,6 @@ package android.os; import static android.system.OsConstants.AF_UNIX; -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.SEEK_SET; import static android.system.OsConstants.SOCK_SEQPACKET; import static android.system.OsConstants.SOCK_STREAM; @@ -254,8 +248,16 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { } /** {@hide} */ - public static ParcelFileDescriptor fromFd( - FileDescriptor fd, Handler handler, final OnCloseListener listener) throws IOException { + public static ParcelFileDescriptor fromPfd(ParcelFileDescriptor pfd, Handler handler, + final OnCloseListener listener) throws IOException { + final FileDescriptor original = new FileDescriptor(); + original.setInt$(pfd.detachFd()); + return fromFd(original, handler, listener); + } + + /** {@hide} */ + public static ParcelFileDescriptor fromFd(FileDescriptor fd, Handler handler, + final OnCloseListener listener) throws IOException { if (handler == null) { throw new IllegalArgumentException("Handler must not be null"); } diff --git a/core/java/android/os/RedactingFileDescriptor.java b/core/java/android/os/RedactingFileDescriptor.java index 60eb5c3c4a89..4e5eaac3442f 100644 --- a/core/java/android/os/RedactingFileDescriptor.java +++ b/core/java/android/os/RedactingFileDescriptor.java @@ -20,15 +20,18 @@ import android.content.Context; import android.os.storage.StorageManager; import android.system.ErrnoException; import android.system.Os; -import android.system.OsConstants; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; + import libcore.io.IoUtils; +import libcore.util.EmptyArray; 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 @@ -40,20 +43,21 @@ public class RedactingFileDescriptor { private static final String TAG = "RedactingFileDescriptor"; private static final boolean DEBUG = true; - private final long[] mRedactRanges; + private volatile long[] mRedactRanges; private FileDescriptor mInner = null; private ParcelFileDescriptor mOuter = null; - private RedactingFileDescriptor(Context context, File file, long[] redactRanges) + private RedactingFileDescriptor(Context context, File file, int mode, long[] redactRanges) throws IOException { mRedactRanges = checkRangesArgument(redactRanges); try { try { - mInner = Os.open(file.getAbsolutePath(), OsConstants.O_RDONLY, 0); + mInner = Os.open(file.getAbsolutePath(), + FileUtils.translateModePfdToPosix(mode), 0); mOuter = context.getSystemService(StorageManager.class) - .openProxyFileDescriptor(ParcelFileDescriptor.MODE_READ_ONLY, mCallback); + .openProxyFileDescriptor(mode, mCallback); } catch (ErrnoException e) { throw e.rethrowAsIOException(); } @@ -78,16 +82,61 @@ public class RedactingFileDescriptor { /** * Open the given {@link File} and returns a {@link ParcelFileDescriptor} - * that offers a redacted, read-only view of the underlying data. + * 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 offsets that should be redacted, stored * as {@code [start1, end1, start2, end2, ...]}. Start values are * inclusive and end values are exclusive. */ - public static ParcelFileDescriptor open(Context context, File file, long[] redactRanges) - throws IOException { - return new RedactingFileDescriptor(context, file, redactRanges).mOuter; + public static ParcelFileDescriptor open(Context context, File file, int mode, + long[] redactRanges) throws IOException { + return new RedactingFileDescriptor(context, file, mode, redactRanges).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. + */ + @VisibleForTesting + public static long[] removeRange(long[] ranges, long start, long end) { + if (start == end) { + return ranges; + } else if (start > end) { + throw new IllegalArgumentException(); + } + + long[] res = EmptyArray.LONG; + 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() { @@ -126,7 +175,24 @@ public class RedactingFileDescriptor { @Override public int onWrite(long offset, int size, byte[] data) throws ErrnoException { - throw new ErrnoException(TAG, OsConstants.EBADF); + 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 diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index e0e4fe29f48a..ec8db1ca580e 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -121,6 +121,8 @@ public final class MediaStore { public static final String PARAM_INCLUDE_PENDING = "includePending"; /** {@hide} */ public static final String PARAM_PROGRESS = "progress"; + /** {@hide} */ + public static final String PARAM_REQUIRE_ORIGINAL = "requireOriginal"; /** * Activity Action: Launch a music player. @@ -478,6 +480,24 @@ public final class MediaStore { } /** + * Update the given {@link Uri} to indicate that the caller requires the + * original file contents when calling + * {@link ContentResolver#openFileDescriptor(Uri, String)}. + * <p> + * This can be useful when the caller wants to ensure they're backing up the + * exact bytes of the underlying media, without any Exif redaction being + * performed. + * <p> + * If the original file contents cannot be provided, a + * {@link UnsupportedOperationException} will be thrown when the returned + * {@link Uri} is used, such as when the caller doesn't hold + * {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION}. + */ + public static @NonNull Uri setRequireOriginal(@NonNull Uri uri) { + return uri.buildUpon().appendQueryParameter(PARAM_REQUIRE_ORIGINAL, "1").build(); + } + + /** * Create a new pending media item using the given parameters. Pending items * are expected to have a short lifetime, and owners should either * {@link PendingSession#publish()} or {@link PendingSession#abandon()} a diff --git a/core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java b/core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java index c8bc35c976a2..9e1523165925 100644 --- a/core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java +++ b/core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java @@ -16,6 +16,10 @@ package android.os; +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; @@ -58,8 +62,8 @@ public class RedactingFileDescriptorTest { @Test public void testSingleByte() throws Exception { - final FileDescriptor fd = RedactingFileDescriptor - .open(mContext, mFile, new long[] { 10, 11 }).getFileDescriptor(); + final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_ONLY, + new long[] { 10, 11 }).getFileDescriptor(); final byte[] buf = new byte[1_000]; assertEquals(buf.length, Os.read(fd, buf, 0, buf.length)); @@ -74,8 +78,8 @@ public class RedactingFileDescriptorTest { @Test public void testRanges() throws Exception { - final FileDescriptor fd = RedactingFileDescriptor - .open(mContext, mFile, new long[] { 100, 200, 300, 400 }).getFileDescriptor(); + final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_ONLY, + new long[] { 100, 200, 300, 400 }).getFileDescriptor(); final byte[] buf = new byte[10]; assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 90)); @@ -96,8 +100,8 @@ public class RedactingFileDescriptorTest { @Test public void testEntireFile() throws Exception { - final FileDescriptor fd = RedactingFileDescriptor - .open(mContext, mFile, new long[] { 0, 5_000_000 }).getFileDescriptor(); + final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_ONLY, + new long[] { 0, 5_000_000 }).getFileDescriptor(); try (FileInputStream in = new FileInputStream(fd)) { int val; @@ -106,4 +110,61 @@ public class RedactingFileDescriptorTest { } } } + + @Test + public void testReadWrite() throws Exception { + final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_WRITE, + new long[] { 100, 200, 300, 400 }).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)); + } } |