From bc2ae008785cc23aa5fa2fe92b1f1d1efef6fb51 Mon Sep 17 00:00:00 2001
From: Jeff Sharkey
Date: Tue, 31 Jul 2018 10:45:37 -0600
Subject: [PATCH] Magic to keep "_data" paths working.
As part of the storage changes in Q, we're removing the ability for
apps to directly access storage devices like /sdcard/. (Instead,
they'll need to go through ContentResolver.openFileDescriptor() to
gain access.) However, in several places we're returning raw
filesystem paths in the "_data" column. An initial attempt to simply
redact these with "/dev/null" shows that many popular apps are
depending on these paths, and become non-functional.
So we need to somehow return "_data" paths that apps can manually
open. We explored tricks like /proc/self/fd/ and FUSE, but neither
of those are feasible. Instead, we've created a cursor that returns
paths of this form:
/mnt/content/media/audio/12
And we then hook Libcore.os to intercept open() syscalls made by
Java code and redirect these to CR.openFileDescriptor() with Uris
like this:
content://media/audio/12
This appears to be enough to keep most popular apps working! Note
that it doesn't support apps that try opening the returned paths
from native code, which we'll hopefully be solving via direct
developer outreach.
Since this feature is a bit risky, it's guarded with a feature flag
that's disabled by default; a future CL will actually enable it,
offering a simple CL to revert in the case of trouble.
Bug: 111268862, 111960973
Test: atest cts/tests/tests/provider/src/android/provider/cts/MediaStore*
Change-Id: Ied15e62b46852aef73725f63d7648da390c4e03e
---
api/current.txt | 2 +
api/system-current.txt | 2 +-
core/java/android/app/ActivityThread.java | 94 ++++++++++++++++++-
.../java/android/content/ContentResolver.java | 38 ++++++++
core/java/android/os/FileUtils.java | 20 ++++
core/java/android/provider/MediaStore.java | 15 +++
.../android/content/ContentResolverTest.java | 15 +++
.../src/android/os/FileUtilsTest.java | 14 +++
8 files changed, 197 insertions(+), 3 deletions(-)
diff --git a/api/current.txt b/api/current.txt
index 15392b8fe29a9..c8d842ddcfb35 100755
--- a/api/current.txt
+++ b/api/current.txt
@@ -36803,10 +36803,12 @@ package android.provider {
method public static android.net.Uri getMediaScannerUri();
method public static android.net.Uri getMediaUri(android.content.Context, android.net.Uri);
method public static java.lang.String getVersion(android.content.Context);
+ method public static java.lang.String getVolumeName(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_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE";
field public static final java.lang.String AUTHORITY = "media";
+ field public static final android.net.Uri AUTHORITY_URI;
field public static final java.lang.String EXTRA_DURATION_LIMIT = "android.intent.extra.durationLimit";
field public static final java.lang.String EXTRA_FINISH_ON_COMPLETION = "android.intent.extra.finishOnCompletion";
field public static final java.lang.String EXTRA_FULL_SCREEN = "android.intent.extra.fullScreen";
diff --git a/api/system-current.txt b/api/system-current.txt
index 4512fc3b9c2b1..fb97643129bd9 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -987,8 +987,8 @@ package android.content {
field public static final java.lang.String ACTION_PRE_BOOT_COMPLETED = "android.intent.action.PRE_BOOT_COMPLETED";
field public static final java.lang.String ACTION_QUERY_PACKAGE_RESTART = "android.intent.action.QUERY_PACKAGE_RESTART";
field public static final java.lang.String ACTION_RESOLVE_INSTANT_APP_PACKAGE = "android.intent.action.RESOLVE_INSTANT_APP_PACKAGE";
- field public static final java.lang.String ACTION_REVIEW_PERMISSION_USAGE = "android.intent.action.REVIEW_PERMISSION_USAGE";
field public static final java.lang.String ACTION_REVIEW_PERMISSIONS = "android.intent.action.REVIEW_PERMISSIONS";
+ field public static final java.lang.String ACTION_REVIEW_PERMISSION_USAGE = "android.intent.action.REVIEW_PERMISSION_USAGE";
field public static final java.lang.String ACTION_SHOW_SUSPENDED_APP_DETAILS = "android.intent.action.SHOW_SUSPENDED_APP_DETAILS";
field public static final deprecated java.lang.String ACTION_SIM_STATE_CHANGED = "android.intent.action.SIM_STATE_CHANGED";
field public static final java.lang.String ACTION_SPLIT_CONFIGURATION_CHANGED = "android.intent.action.SPLIT_CONFIGURATION_CHANGED";
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 4756bf40bad3b..ee7d00208e2c6 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -23,6 +23,8 @@ import static android.app.servertransaction.ActivityLifecycleItem.ON_RESUME;
import static android.app.servertransaction.ActivityLifecycleItem.ON_START;
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE;
+import static android.content.ContentResolver.DEPRECATE_DATA_COLUMNS;
+import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX;
import static android.view.Display.INVALID_DISPLAY;
import android.annotation.NonNull;
@@ -45,6 +47,7 @@ import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
import android.content.ContentProvider;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentProvider;
import android.content.IIntentReceiver;
@@ -84,6 +87,7 @@ import android.os.Bundle;
import android.os.Debug;
import android.os.DropBoxManager;
import android.os.Environment;
+import android.os.FileUtils;
import android.os.GraphicsEnvironment;
import android.os.Handler;
import android.os.HandlerExecutor;
@@ -114,6 +118,9 @@ import android.provider.Settings;
import android.renderscript.RenderScriptCacheDir;
import android.security.NetworkSecurityPolicy;
import android.security.net.config.NetworkSecurityConfigProvider;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.system.StructStat;
import android.util.AndroidRuntimeException;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
@@ -162,13 +169,16 @@ import dalvik.system.VMRuntime;
import libcore.io.DropBox;
import libcore.io.EventLogger;
+import libcore.io.ForwardingOs;
import libcore.io.IoUtils;
+import libcore.io.Os;
import libcore.net.event.NetworkEventDispatcher;
import org.apache.harmony.dalvik.ddmc.DdmVmInternal;
import java.io.File;
import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
@@ -6749,7 +6759,7 @@ public final class ActivityThread extends ClientTransactionHandler {
}
}
- private class DropBoxReporter implements DropBox.Reporter {
+ private static class DropBoxReporter implements DropBox.Reporter {
private DropBoxManager dropBox;
@@ -6769,7 +6779,84 @@ public final class ActivityThread extends ClientTransactionHandler {
private synchronized void ensureInitialized() {
if (dropBox == null) {
- dropBox = (DropBoxManager) getSystemContext().getSystemService(Context.DROPBOX_SERVICE);
+ dropBox = currentActivityThread().getApplication()
+ .getSystemService(DropBoxManager.class);
+ }
+ }
+ }
+
+ private static class AndroidOs extends ForwardingOs {
+ /**
+ * Install selective syscall interception. For example, this is used to
+ * implement special filesystem paths that will be redirected to
+ * {@link ContentResolver#openFileDescriptor(Uri, String)}.
+ */
+ public static void install() {
+ // If feature is disabled, we don't need to install
+ if (!DEPRECATE_DATA_COLUMNS) return;
+
+ // If app is modern enough, we don't need to install
+ if (VMRuntime.getRuntime().getTargetSdkVersion() >= Build.VERSION_CODES.Q) return;
+
+ // Install interception and make sure it sticks!
+ Os def = null;
+ do {
+ def = Os.getDefault();
+ } while (!Os.compareAndSetDefault(def, new AndroidOs(def)));
+ }
+
+ private AndroidOs(Os os) {
+ super(os);
+ }
+
+ private FileDescriptor openDeprecatedDataPath(String path, int mode) throws ErrnoException {
+ final Uri uri = ContentResolver.translateDeprecatedDataPath(path);
+ Log.v(TAG, "Redirecting " + path + " to " + uri);
+
+ final ContentResolver cr = currentActivityThread().getApplication()
+ .getContentResolver();
+ try {
+ final FileDescriptor fd = new FileDescriptor();
+ fd.setInt$(cr.openFileDescriptor(uri,
+ FileUtils.translateModePosixToString(mode)).detachFd());
+ return fd;
+ } catch (FileNotFoundException e) {
+ throw new ErrnoException(e.getMessage(), OsConstants.ENOENT);
+ }
+ }
+
+ @Override
+ public boolean access(String path, int mode) throws ErrnoException {
+ if (path != null && path.startsWith(DEPRECATE_DATA_PREFIX)) {
+ // If we opened it okay, then access check succeeded
+ IoUtils.closeQuietly(
+ openDeprecatedDataPath(path, FileUtils.translateModeAccessToPosix(mode)));
+ return true;
+ } else {
+ return super.access(path, mode);
+ }
+ }
+
+ @Override
+ public FileDescriptor open(String path, int flags, int mode) throws ErrnoException {
+ if (path != null && path.startsWith(DEPRECATE_DATA_PREFIX)) {
+ return openDeprecatedDataPath(path, mode);
+ } else {
+ return super.open(path, flags, mode);
+ }
+ }
+
+ @Override
+ public StructStat stat(String path) throws ErrnoException {
+ if (path != null && path.startsWith(DEPRECATE_DATA_PREFIX)) {
+ final FileDescriptor fd = openDeprecatedDataPath(path, OsConstants.O_RDONLY);
+ try {
+ return android.system.Os.fstat(fd);
+ } finally {
+ IoUtils.closeQuietly(fd);
+ }
+ } else {
+ return super.stat(path);
}
}
}
@@ -6777,6 +6864,9 @@ public final class ActivityThread extends ClientTransactionHandler {
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
+ // Install selective syscall interception
+ AndroidOs.install();
+
// CloseGuard defaults to true and can be quite spammy. We
// disable it here, but selectively enable it later (via
// StrictMode) on debug builds, but using DropBox, not logs.
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index a2a6b9b4a7624..4de1dfcc12ba2 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -52,7 +52,9 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.storage.StorageManager;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
@@ -87,6 +89,30 @@ import java.util.concurrent.atomic.AtomicBoolean;
* developer guide.
*/
public abstract class ContentResolver {
+ /**
+ * Enables logic that supports deprecation of {@code _data} columns,
+ * typically by replacing values with fake paths that the OS then offers to
+ * redirect to {@link #openFileDescriptor(Uri, String)}, which developers
+ * should be using directly.
+ *
+ * @hide
+ */
+ public static final boolean DEPRECATE_DATA_COLUMNS = SystemProperties
+ .getBoolean(StorageManager.PROP_ISOLATED_STORAGE, false);
+
+ /**
+ * Special filesystem path prefix which indicates that a path should be
+ * treated as a {@code content://} {@link Uri} when
+ * {@link #DEPRECATE_DATA_COLUMNS} is enabled.
+ *
+ * The remainder of the path after this prefix is a
+ * {@link Uri#getSchemeSpecificPart()} value, which includes authority, path
+ * segments, and query parameters.
+ *
+ * @hide
+ */
+ public static final String DEPRECATE_DATA_PREFIX = "/mnt/content/";
+
/**
* @deprecated instead use
* {@link #requestSync(android.accounts.Account, String, android.os.Bundle)}
@@ -3261,4 +3287,16 @@ public abstract class ContentResolver {
e.rethrowFromSystemServer();
}
}
+
+ /** {@hide} */
+ public static Uri translateDeprecatedDataPath(String path) {
+ final String ssp = "//" + path.substring(DEPRECATE_DATA_PREFIX.length());
+ return Uri.parse(new Uri.Builder().scheme(SCHEME_CONTENT)
+ .encodedOpaquePart(ssp).build().toString());
+ }
+
+ /** {@hide} */
+ public static String translateDeprecatedDataPath(Uri uri) {
+ return DEPRECATE_DATA_PREFIX + uri.getEncodedSchemeSpecificPart().substring(2);
+ }
}
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index f71fdd7fdac1f..0b90f54378261 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -22,16 +22,19 @@ 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_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.SPLICE_F_MORE;
import static android.system.OsConstants.SPLICE_F_MOVE;
import static android.system.OsConstants.S_ISFIFO;
import static android.system.OsConstants.S_ISREG;
+import static android.system.OsConstants.W_OK;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -1299,6 +1302,23 @@ public class FileUtils {
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);
+ }
+ }
+
/** {@hide} */
@VisibleForTesting
public static class MemoryPipe extends Thread implements AutoCloseable {
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index 0aab76ebd0e07..1fce8e6c9ac22 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -60,7 +60,10 @@ import java.util.List;
public final class MediaStore {
private final static String TAG = "MediaStore";
+ /** The authority for the media provider */
public static final String AUTHORITY = "media";
+ /** A content:// style uri to the authority for the media provider */
+ public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/";
@@ -2253,6 +2256,18 @@ public final class MediaStore {
}
}
+ /**
+ * Return the volume name that the given {@link Uri} references.
+ */
+ public static @NonNull String getVolumeName(@NonNull Uri uri) {
+ final List segments = uri.getPathSegments();
+ if (uri.getAuthority().equals(AUTHORITY) && segments != null && segments.size() > 0) {
+ return segments.get(0);
+ } else {
+ throw new IllegalArgumentException("Not a media Uri: " + uri);
+ }
+ }
+
/**
* Uri for querying the state of the media scanner.
*/
diff --git a/core/tests/coretests/src/android/content/ContentResolverTest.java b/core/tests/coretests/src/android/content/ContentResolverTest.java
index 0036186994fe8..9940bf7dd692b 100644
--- a/core/tests/coretests/src/android/content/ContentResolverTest.java
+++ b/core/tests/coretests/src/android/content/ContentResolverTest.java
@@ -158,4 +158,19 @@ public class ContentResolverTest {
assertImageAspectAndContents(res);
}
+
+ @Test
+ public void testTranslateDeprecatedDataPath() throws Exception {
+ assertTranslate(Uri.parse("content://com.example/path/?foo=bar&baz=meow"));
+ assertTranslate(Uri.parse("content://com.example/path/subpath/12/"));
+ assertTranslate(Uri.parse("content://com.example/path/subpath/12"));
+ assertTranslate(Uri.parse("content://com.example/path/12"));
+ assertTranslate(Uri.parse("content://com.example/"));
+ assertTranslate(Uri.parse("content://com.example"));
+ }
+
+ private static void assertTranslate(Uri uri) {
+ assertEquals(uri, ContentResolver
+ .translateDeprecatedDataPath(ContentResolver.translateDeprecatedDataPath(uri)));
+ }
}
diff --git a/core/tests/coretests/src/android/os/FileUtilsTest.java b/core/tests/coretests/src/android/os/FileUtilsTest.java
index 6966448f7d63c..55e21a76f170e 100644
--- a/core/tests/coretests/src/android/os/FileUtilsTest.java
+++ b/core/tests/coretests/src/android/os/FileUtilsTest.java
@@ -17,6 +17,7 @@
package android.os;
import static android.os.FileUtils.roundStorageSize;
+import static android.os.FileUtils.translateModeAccessToPosix;
import static android.os.FileUtils.translateModePfdToPosix;
import static android.os.FileUtils.translateModePosixToPfd;
import static android.os.FileUtils.translateModePosixToString;
@@ -27,12 +28,16 @@ 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_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 static android.system.OsConstants.X_OK;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
@@ -525,6 +530,15 @@ public class FileUtilsTest {
}
}
+ @Test
+ public void testTranslateMode_Access() throws Exception {
+ assertEquals(O_RDONLY, translateModeAccessToPosix(F_OK));
+ assertEquals(O_RDONLY, translateModeAccessToPosix(R_OK));
+ assertEquals(O_WRONLY, translateModeAccessToPosix(W_OK));
+ assertEquals(O_RDWR, translateModeAccessToPosix(R_OK | W_OK));
+ assertEquals(O_RDWR, translateModeAccessToPosix(R_OK | W_OK | X_OK));
+ }
+
private static void assertTranslate(String string, int posix, int pfd) {
assertEquals(posix, translateModeStringToPosix(string));
assertEquals(string, translateModePosixToString(posix));