Loading src/com/android/documentsui/AbstractActionHandler.java +31 −15 Original line number Diff line number Diff line Loading @@ -779,32 +779,48 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA } protected final boolean launchToDocument(Uri uri) { if (DEBUG) { Log.d(TAG, "launchToDocument() uri=" + uri); } // We don't support launching to a document in an archive. if (!Providers.isArchiveUri(uri)) { loadDocument(uri, UserId.DEFAULT_USER, this::onStackLoaded); if (Providers.isArchiveUri(uri)) { return false; } loadDocument(uri, UserId.DEFAULT_USER, this::onStackToLaunchToLoaded); return true; } return false; /** * Invoked <b>only</b> once, when the initial stack (that is the stack we are going to * "launch to") is loaded. * * @see #launchToDocument(Uri) */ private void onStackToLaunchToLoaded(@Nullable DocumentStack stack) { if (DEBUG) { Log.d(TAG, "onLaunchStackLoaded() stack=" + stack); } if (stack == null) { Log.w(TAG, "Failed to launch into the given uri. Launch to default location."); launchToDefaultLocation(); Metrics.logLaunchAtLocation(mState, null); return; } private void onStackLoaded(@Nullable DocumentStack stack) { if (stack != null) { // Make sure the document at the top of the stack is a directory (if it isn't - just pop // one off). if (!stack.peek().isDirectory()) { // Requested document is not a directory. Pop it so that we can launch into its // parent. stack.pop(); } mState.stack.reset(stack); mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); Metrics.logLaunchAtLocation(mState, stack.getRoot().getUri()); } else { Log.w(TAG, "Failed to launch into the given uri. Launch to default location."); launchToDefaultLocation(); Metrics.logLaunchAtLocation(mState, null); } } private void onRootLoaded(@Nullable RootInfo root) { Loading src/com/android/documentsui/picker/ActionHandler.java +102 −2 Original line number Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.documentsui.picker; import static android.provider.DocumentsContract.isDocumentUri; import static android.provider.DocumentsContract.isRootUri; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.State.ACTION_CREATE; import static com.android.documentsui.base.State.ACTION_GET_CONTENT; Loading @@ -23,6 +26,8 @@ import static com.android.documentsui.base.State.ACTION_OPEN; import static com.android.documentsui.base.State.ACTION_OPEN_TREE; import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION; import static java.util.regex.Pattern.CASE_INSENSITIVE; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ComponentName; Loading @@ -35,6 +40,8 @@ import android.provider.DocumentsContract; import android.provider.Settings; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; Loading @@ -52,6 +59,7 @@ import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.Features; import com.android.documentsui.base.Lookup; import com.android.documentsui.base.Providers; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.Shared; import com.android.documentsui.base.State; Loading @@ -61,11 +69,14 @@ import com.android.documentsui.picker.ActionHandler.Addons; import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.roots.ProvidersAccess; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.util.FileUtils; import java.io.IOException; import java.util.Arrays; import java.util.Locale; import java.util.concurrent.Executor; import java.util.regex.Pattern; import javax.annotation.Nullable; /** * Provides {@link PickActivity} action specializations to fragments. Loading @@ -74,6 +85,20 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH private static final String TAG = "PickerActionHandler"; /** * Used to prevent applications from using {@link Intent.ACTION_OPEN_DOCUMENT_TREE} and * the {@link Intent.ACTION_OPEN_DOCUMENT} actions to request that the user select individual * files from "/Android/data", "/Android/obb", "/Android/sandbox" directories and all their * subdirectories (on the external storage), in accordance with the SAF privacy restrictions * introduced in Android 11 (R). * * <p> * See <a href="https://developer.android.com/about/versions/11/privacy/storage#file-access"> * Storage updates in Android 11</a>. */ private static final Pattern PATTERN_RESTRICTED_INITIAL_PATH = Pattern.compile("^/Android/(?:data|obb|sandbox).*", CASE_INSENSITIVE); private final Features mFeatures; private final ActivityConfig mConfig; private final LastAccessedStorage mLastAccessed; Loading Loading @@ -165,9 +190,81 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH } } final boolean isRoot = isRootUri(mActivity, uri); final boolean isDocument = !isRoot && isDocumentUri(mActivity, uri); if (!isRoot && !isDocument) { // Neither a root nor a document. return false; } if (isRoot) { loadRoot(uri, UserId.DEFAULT_USER); return true; } // From here onwards: isDoc == true. if (shouldPreemptivelyRestrictRequestedInitialUri(uri)) { Log.w(TAG, "Requested initial URI - " + uri + " - is restricted: " + "loading device root instead."); return false; } return launchToDocument(uri); } /** * Starting with Android 11 (R, API Level 30) applications are no longer allowed to use the * {@link Intent#ACTION_OPEN_DOCUMENT} and {@link Intent#ACTION_OPEN_DOCUMENT_TREE} to request * that the user select individual files from "Android/data/", "Android/obb/", * "Android/sandbox/" directories and all their subdirectories on "external storage". * <p> * See <a href="https://developer.android.com/about/versions/11/privacy/storage#file-access"> * Storage updates in Android 11</a>. * <p> * Ideally, this should be handled on the {@code ExternalStorageProvider} side, but as of * Android 14 (U) FRC, {@code ExternalStorageProvider} "hides" only "Android/data/", * "Android/obb/" and "Android/sandbox/" directories, but NOT their subdirectories. */ private boolean shouldPreemptivelyRestrictRequestedInitialUri(@NonNull Uri uri) { // Not restricting SAF access for the calling app. if (!Shared.shouldRestrictStorageAccessFramework(mActivity)) { return false; } // We only need to restrict some locations on the "external" storage. if (!Providers.AUTHORITY_STORAGE.equals(uri.getAuthority())) { return false; } // TODO(b/283962634): in the future this will have to be platform-version specific. // For example, if the fix on the ExternalStorageProvider side makes it to the Android 15, // we would change this to check if the platform version >= 15. // In the upcoming Android 14 release, however, ExternalStorageProvider does NOT yet // implement this logic. final boolean externalProviderImplementsSafRestrictions = false; if (externalProviderImplementsSafRestrictions) { return false; } // External Storage Provider's docId format is "root:path/to/file" // The getPathFromStorageDocId() turns that into "/path/to/file" // Note the missing leading "/" in the path part of the docId, while the path returned by // the getPathFromStorageDocId() start with "/". final String docId = DocumentsContract.getDocumentId(uri); final String filePath; try { filePath = FileUtils.getPathFromStorageDocId(docId); } catch (IOException e) { Log.w(TAG, "Could not get canonical file path from docId '" + docId + "'"); return true; } // Check if the app is asking for /Android/data, /Android/obb, /Android/sandbox or any of // their subdirectories (on the external storage). return PATTERN_RESTRICTED_INITIAL_PATH.matcher(filePath).matches(); } private void initLoadLastAccessedStack() { if (DEBUG) { Log.d(TAG, "Attempting to load last used stack for calling package."); Loading @@ -188,6 +285,9 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH private void onLastAccessedStackLoaded(@Nullable DocumentStack stack) { if (stack == null) { loadDefaultLocation(); } else if (shouldPreemptivelyRestrictRequestedInitialUri(stack.peek().getDocumentUri())) { // If the last accessed stack has restricted uri, load default location loadDefaultLocation(); } else { mState.stack.reset(stack); mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); Loading src/com/android/documentsui/util/FileUtils.java 0 → 100644 +58 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.documentsui.util; import androidx.annotation.NonNull; import java.io.File; import java.io.IOException; import java.util.Objects; public class FileUtils { /** * Returns the canonical pathname string of the provided abstract pathname. * * @return The canonical pathname string denoting the same file or directory as this abstract * pathname. * @see File#getCanonicalPath() */ @NonNull public static String getCanonicalPath(@NonNull String path) throws IOException { Objects.requireNonNull(path); return new File(path).getCanonicalPath(); } /** * This is basically a very slightly tweaked fork of * {@link com.android.externalstorage.ExternalStorageProvider#getPathFromDocId(String)}. * The difference between this fork and the "original" method is that here we do not strip * the leading and trailing "/"s (because we don't worry about those). * * @return canonicalized file path. */ public static String getPathFromStorageDocId(String docId) throws IOException { // Remove the root tag from the docId, e.g. "primary:", which should leave with the file // path. final String docIdPath = docId.substring(docId.indexOf(':', 1) + 1); return getCanonicalPath(docIdPath); } private FileUtils() { } } Loading
src/com/android/documentsui/AbstractActionHandler.java +31 −15 Original line number Diff line number Diff line Loading @@ -779,32 +779,48 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA } protected final boolean launchToDocument(Uri uri) { if (DEBUG) { Log.d(TAG, "launchToDocument() uri=" + uri); } // We don't support launching to a document in an archive. if (!Providers.isArchiveUri(uri)) { loadDocument(uri, UserId.DEFAULT_USER, this::onStackLoaded); if (Providers.isArchiveUri(uri)) { return false; } loadDocument(uri, UserId.DEFAULT_USER, this::onStackToLaunchToLoaded); return true; } return false; /** * Invoked <b>only</b> once, when the initial stack (that is the stack we are going to * "launch to") is loaded. * * @see #launchToDocument(Uri) */ private void onStackToLaunchToLoaded(@Nullable DocumentStack stack) { if (DEBUG) { Log.d(TAG, "onLaunchStackLoaded() stack=" + stack); } if (stack == null) { Log.w(TAG, "Failed to launch into the given uri. Launch to default location."); launchToDefaultLocation(); Metrics.logLaunchAtLocation(mState, null); return; } private void onStackLoaded(@Nullable DocumentStack stack) { if (stack != null) { // Make sure the document at the top of the stack is a directory (if it isn't - just pop // one off). if (!stack.peek().isDirectory()) { // Requested document is not a directory. Pop it so that we can launch into its // parent. stack.pop(); } mState.stack.reset(stack); mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); Metrics.logLaunchAtLocation(mState, stack.getRoot().getUri()); } else { Log.w(TAG, "Failed to launch into the given uri. Launch to default location."); launchToDefaultLocation(); Metrics.logLaunchAtLocation(mState, null); } } private void onRootLoaded(@Nullable RootInfo root) { Loading
src/com/android/documentsui/picker/ActionHandler.java +102 −2 Original line number Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.documentsui.picker; import static android.provider.DocumentsContract.isDocumentUri; import static android.provider.DocumentsContract.isRootUri; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.State.ACTION_CREATE; import static com.android.documentsui.base.State.ACTION_GET_CONTENT; Loading @@ -23,6 +26,8 @@ import static com.android.documentsui.base.State.ACTION_OPEN; import static com.android.documentsui.base.State.ACTION_OPEN_TREE; import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION; import static java.util.regex.Pattern.CASE_INSENSITIVE; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ComponentName; Loading @@ -35,6 +40,8 @@ import android.provider.DocumentsContract; import android.provider.Settings; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; Loading @@ -52,6 +59,7 @@ import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.Features; import com.android.documentsui.base.Lookup; import com.android.documentsui.base.Providers; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.Shared; import com.android.documentsui.base.State; Loading @@ -61,11 +69,14 @@ import com.android.documentsui.picker.ActionHandler.Addons; import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.roots.ProvidersAccess; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.util.FileUtils; import java.io.IOException; import java.util.Arrays; import java.util.Locale; import java.util.concurrent.Executor; import java.util.regex.Pattern; import javax.annotation.Nullable; /** * Provides {@link PickActivity} action specializations to fragments. Loading @@ -74,6 +85,20 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH private static final String TAG = "PickerActionHandler"; /** * Used to prevent applications from using {@link Intent.ACTION_OPEN_DOCUMENT_TREE} and * the {@link Intent.ACTION_OPEN_DOCUMENT} actions to request that the user select individual * files from "/Android/data", "/Android/obb", "/Android/sandbox" directories and all their * subdirectories (on the external storage), in accordance with the SAF privacy restrictions * introduced in Android 11 (R). * * <p> * See <a href="https://developer.android.com/about/versions/11/privacy/storage#file-access"> * Storage updates in Android 11</a>. */ private static final Pattern PATTERN_RESTRICTED_INITIAL_PATH = Pattern.compile("^/Android/(?:data|obb|sandbox).*", CASE_INSENSITIVE); private final Features mFeatures; private final ActivityConfig mConfig; private final LastAccessedStorage mLastAccessed; Loading Loading @@ -165,9 +190,81 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH } } final boolean isRoot = isRootUri(mActivity, uri); final boolean isDocument = !isRoot && isDocumentUri(mActivity, uri); if (!isRoot && !isDocument) { // Neither a root nor a document. return false; } if (isRoot) { loadRoot(uri, UserId.DEFAULT_USER); return true; } // From here onwards: isDoc == true. if (shouldPreemptivelyRestrictRequestedInitialUri(uri)) { Log.w(TAG, "Requested initial URI - " + uri + " - is restricted: " + "loading device root instead."); return false; } return launchToDocument(uri); } /** * Starting with Android 11 (R, API Level 30) applications are no longer allowed to use the * {@link Intent#ACTION_OPEN_DOCUMENT} and {@link Intent#ACTION_OPEN_DOCUMENT_TREE} to request * that the user select individual files from "Android/data/", "Android/obb/", * "Android/sandbox/" directories and all their subdirectories on "external storage". * <p> * See <a href="https://developer.android.com/about/versions/11/privacy/storage#file-access"> * Storage updates in Android 11</a>. * <p> * Ideally, this should be handled on the {@code ExternalStorageProvider} side, but as of * Android 14 (U) FRC, {@code ExternalStorageProvider} "hides" only "Android/data/", * "Android/obb/" and "Android/sandbox/" directories, but NOT their subdirectories. */ private boolean shouldPreemptivelyRestrictRequestedInitialUri(@NonNull Uri uri) { // Not restricting SAF access for the calling app. if (!Shared.shouldRestrictStorageAccessFramework(mActivity)) { return false; } // We only need to restrict some locations on the "external" storage. if (!Providers.AUTHORITY_STORAGE.equals(uri.getAuthority())) { return false; } // TODO(b/283962634): in the future this will have to be platform-version specific. // For example, if the fix on the ExternalStorageProvider side makes it to the Android 15, // we would change this to check if the platform version >= 15. // In the upcoming Android 14 release, however, ExternalStorageProvider does NOT yet // implement this logic. final boolean externalProviderImplementsSafRestrictions = false; if (externalProviderImplementsSafRestrictions) { return false; } // External Storage Provider's docId format is "root:path/to/file" // The getPathFromStorageDocId() turns that into "/path/to/file" // Note the missing leading "/" in the path part of the docId, while the path returned by // the getPathFromStorageDocId() start with "/". final String docId = DocumentsContract.getDocumentId(uri); final String filePath; try { filePath = FileUtils.getPathFromStorageDocId(docId); } catch (IOException e) { Log.w(TAG, "Could not get canonical file path from docId '" + docId + "'"); return true; } // Check if the app is asking for /Android/data, /Android/obb, /Android/sandbox or any of // their subdirectories (on the external storage). return PATTERN_RESTRICTED_INITIAL_PATH.matcher(filePath).matches(); } private void initLoadLastAccessedStack() { if (DEBUG) { Log.d(TAG, "Attempting to load last used stack for calling package."); Loading @@ -188,6 +285,9 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH private void onLastAccessedStackLoaded(@Nullable DocumentStack stack) { if (stack == null) { loadDefaultLocation(); } else if (shouldPreemptivelyRestrictRequestedInitialUri(stack.peek().getDocumentUri())) { // If the last accessed stack has restricted uri, load default location loadDefaultLocation(); } else { mState.stack.reset(stack); mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); Loading
src/com/android/documentsui/util/FileUtils.java 0 → 100644 +58 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.documentsui.util; import androidx.annotation.NonNull; import java.io.File; import java.io.IOException; import java.util.Objects; public class FileUtils { /** * Returns the canonical pathname string of the provided abstract pathname. * * @return The canonical pathname string denoting the same file or directory as this abstract * pathname. * @see File#getCanonicalPath() */ @NonNull public static String getCanonicalPath(@NonNull String path) throws IOException { Objects.requireNonNull(path); return new File(path).getCanonicalPath(); } /** * This is basically a very slightly tweaked fork of * {@link com.android.externalstorage.ExternalStorageProvider#getPathFromDocId(String)}. * The difference between this fork and the "original" method is that here we do not strip * the leading and trailing "/"s (because we don't worry about those). * * @return canonicalized file path. */ public static String getPathFromStorageDocId(String docId) throws IOException { // Remove the root tag from the docId, e.g. "primary:", which should leave with the file // path. final String docIdPath = docId.substring(docId.indexOf(':', 1) + 1); return getCanonicalPath(docIdPath); } private FileUtils() { } }