summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Amin Shaikh <ashaikh@google.com> 2018-10-23 14:22:48 -0400
committer Amin Shaikh <ashaikh@google.com> 2018-11-02 15:12:55 -0400
commitced7a45a757c48271a9cf630f64ae7cdfa3c6d7d (patch)
treee9b4fec9ada065124359e123d7933c36e8474f49
parent8d8d92f38f2a92cc914b3e59648f3eee7ce9af47 (diff)
Deprecate scoped directory access.
- Change ScopedAccessActivity to immediately return RESULT_CANCELLED - Delete all unused code/resources associated with this activity - Delete ScopedAccessProvider - Log all launches of android.os.storage.action.OPEN_EXTERNAL_DIRECTORY intents in Q+ to a "docsui_scoped_directory_access_deprecated" counter Bug: 111892460 Test: atest \ cts/tests/providerui/src/android/providerui/cts/MediaStoreUiTest.java \ cts/tests/tests/os/src/android/os/storage/cts/StorageManagerTest.java \ cts/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/ScopedDirectoryAccessClientTest.java Change-Id: I9096f3490ba303bcdc557e0e86ce142593dfe9b1
-rw-r--r--Android.bp2
-rw-r--r--AndroidManifest.xml7
-rw-r--r--minimal/AndroidManifest.xml15
-rw-r--r--minimal/res/layout/dialog_open_scoped_directory.xml44
-rw-r--r--minimal/res/values/strings.xml2
-rw-r--r--res/layout/dialog_open_scoped_directory.xml44
-rw-r--r--res/values/strings.xml2
-rw-r--r--src/com/android/documentsui/DocumentsApplication.java2
-rw-r--r--src/com/android/documentsui/PackageReceiver.java8
-rw-r--r--src/com/android/documentsui/ScopedAccessActivity.java358
-rw-r--r--src/com/android/documentsui/ScopedAccessMetrics.java18
-rw-r--r--src/com/android/documentsui/ScopedAccessPackageReceiver.java46
-rw-r--r--src/com/android/documentsui/ScopedAccessProvider.java600
-rw-r--r--src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java195
14 files changed, 27 insertions, 1316 deletions
diff --git a/Android.bp b/Android.bp
index 976d1e42f..1458d94fa 100644
--- a/Android.bp
+++ b/Android.bp
@@ -37,8 +37,6 @@ android_app {
"minimal/src/com/android/documentsui/picker/DummyPickActivity.java",
"src/com/android/documentsui/ScopedAccessActivity.java",
"src/com/android/documentsui/ScopedAccessMetrics.java",
- "src/com/android/documentsui/ScopedAccessPackageReceiver.java",
- "src/com/android/documentsui/ScopedAccessProvider.java",
"src/com/android/documentsui/archives/Archive.java",
"src/com/android/documentsui/archives/ArchiveId.java",
"src/com/android/documentsui/archives/ArchivesProvider.java",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c26428557..964466be9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -138,13 +138,6 @@
</activity>
<provider
- android:name=".ScopedAccessProvider"
- android:authorities="com.android.documentsui.scopedAccess"
- android:permission="android.permission.MANAGE_SCOPED_ACCESS_DIRECTORY_PERMISSIONS"
- android:exported="true">
- </provider>
-
- <provider
android:name=".picker.LastAccessedProvider"
android:authorities="com.android.documentsui.lastAccessed"
android:exported="false"/>
diff --git a/minimal/AndroidManifest.xml b/minimal/AndroidManifest.xml
index 95c8621e9..4dafeee3e 100644
--- a/minimal/AndroidManifest.xml
+++ b/minimal/AndroidManifest.xml
@@ -54,20 +54,5 @@
</intent-filter>
</activity>
- <receiver android:name=".ScopedAccessPackageReceiver">
- <intent-filter>
- <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
- <action android:name="android.intent.action.PACKAGE_DATA_CLEARED" />
- <data android:scheme="package" />
- </intent-filter>
- </receiver>
-
- <provider
- android:name=".ScopedAccessProvider"
- android:authorities="com.android.documentsui.scopedAccess"
- android:permission="android.permission.MANAGE_SCOPED_ACCESS_DIRECTORY_PERMISSIONS"
- android:exported="true">
- </provider>
-
</application>
</manifest>
diff --git a/minimal/res/layout/dialog_open_scoped_directory.xml b/minimal/res/layout/dialog_open_scoped_directory.xml
deleted file mode 100644
index cb3920604..000000000
--- a/minimal/res/layout/dialog_open_scoped_directory.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2017 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.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:theme="@style/Theme.AppCompat.Light.Dialog.Alert"
- android:orientation="vertical"
- android:paddingEnd="24dp"
- android:paddingStart="24dp" >
-
- <TextView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/message"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingEnd="24dp"
- android:paddingStart="32dp"
- android:paddingTop="24dp">
- </TextView>
-
- <CheckBox
- android:id="@+id/do_not_ask_checkbox"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="16dip"
- android:text="@string/never_ask_again"
- android:textColor="?android:attr/textColorSecondary"
- android:visibility="gone" />
-</LinearLayout>
diff --git a/minimal/res/values/strings.xml b/minimal/res/values/strings.xml
index 026b75a33..2c6f4e59e 100644
--- a/minimal/res/values/strings.xml
+++ b/minimal/res/values/strings.xml
@@ -34,8 +34,6 @@
<!-- Text in an alert dialog asking user to grant app access to all data in an external storage volume -->
<string name="open_external_dialog_root_request">Grant <xliff:g id="appName" example="System Settings"><b>^1</b></xliff:g>
access to your data, including photos and videos, on <xliff:g id="storage" example="SD Card"><i>^2</i></xliff:g>?</string>
- <!-- Checkbox that allows user to not be questioned about the directory access request again -->
- <string name="never_ask_again">Don\'t ask again</string>
<!-- Text in the button asking user to allow access to a given directory. -->
<string name="allow">Allow</string>
<!-- Text in the button asking user to deny access to a given directory. -->
diff --git a/res/layout/dialog_open_scoped_directory.xml b/res/layout/dialog_open_scoped_directory.xml
deleted file mode 100644
index bfb027149..000000000
--- a/res/layout/dialog_open_scoped_directory.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2016 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.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:theme="@style/Theme.AppCompat.Light.Dialog.Alert"
- android:orientation="vertical"
- android:paddingEnd="24dp"
- android:paddingStart="24dp" >
-
- <TextView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/message"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingEnd="24dp"
- android:paddingStart="32dp"
- android:paddingTop="24dp">
- </TextView>
-
- <CheckBox
- android:id="@+id/do_not_ask_checkbox"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="16dip"
- android:text="@string/never_ask_again"
- android:textColor="?android:attr/textColorSecondary"
- android:visibility="gone" />
-</LinearLayout> \ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 296c6f7d6..c207bebaa 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -330,8 +330,6 @@
<!-- Text in an alert dialog asking user to grant app access to all data in an external storage volume -->
<string name="open_external_dialog_root_request">Grant <xliff:g id="appName" example="System Settings"><b>^1</b></xliff:g>
access to your data, including photos and videos, on <xliff:g id="storage" example="SD Card"><i>^2</i></xliff:g>?</string>
- <!-- Checkbox that allows user to not be questioned about the directory access request again -->
- <string name="never_ask_again">Don\'t ask again</string>
<!-- Text in the button asking user to allow access to a given directory. -->
<string name="allow">Allow</string>
<!-- Text in the button asking user to deny access to a given directory. -->
diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java
index 4c9c65f2f..41eda6523 100644
--- a/src/com/android/documentsui/DocumentsApplication.java
+++ b/src/com/android/documentsui/DocumentsApplication.java
@@ -32,6 +32,7 @@ import com.android.documentsui.base.Lookup;
import com.android.documentsui.clipping.ClipStorage;
import com.android.documentsui.clipping.ClipStore;
import com.android.documentsui.clipping.DocumentClipper;
+import com.android.documentsui.prefs.ScopedAccessLocalPreferences;
import com.android.documentsui.roots.ProvidersCache;
public class DocumentsApplication extends Application {
@@ -112,6 +113,7 @@ public class DocumentsApplication extends Application {
final IntentFilter localeFilter = new IntentFilter();
localeFilter.addAction(Intent.ACTION_LOCALE_CHANGED);
registerReceiver(mCacheReceiver, localeFilter);
+ ScopedAccessLocalPreferences.clearScopedAccessPreferences(this);
}
@Override
diff --git a/src/com/android/documentsui/PackageReceiver.java b/src/com/android/documentsui/PackageReceiver.java
index e917369d9..3153e9b42 100644
--- a/src/com/android/documentsui/PackageReceiver.java
+++ b/src/com/android/documentsui/PackageReceiver.java
@@ -23,11 +23,9 @@ import android.content.Intent;
import android.net.Uri;
import com.android.documentsui.picker.LastAccessedProvider;
-import com.android.documentsui.prefs.ScopedAccessLocalPreferences;
/**
- * Clean up {@link LastAccessedProvider} and {@link ScopedAccessLocalPreferences} when packages
- * are removed.
+ * Clean up {@link LastAccessedProvider} when packages are removed.
*/
public class PackageReceiver extends BroadcastReceiver {
@Override
@@ -44,16 +42,12 @@ public class PackageReceiver extends BroadcastReceiver {
LastAccessedProvider.METHOD_PURGE,
null,
null);
- if (packageName != null) {
- ScopedAccessLocalPreferences.clearPackagePreferences(context, packageName);
- }
} else if (Intent.ACTION_PACKAGE_DATA_CLEARED.equals(action)) {
if (packageName != null) {
resolver.call(
LastAccessedProvider.buildLastAccessed(packageName),
LastAccessedProvider.METHOD_PURGE_PACKAGE,
packageName, null);
- ScopedAccessLocalPreferences.clearPackagePreferences(context, packageName);
}
}
}
diff --git a/src/com/android/documentsui/ScopedAccessActivity.java b/src/com/android/documentsui/ScopedAccessActivity.java
index 3e606bc61..6deab4ea8 100644
--- a/src/com/android/documentsui/ScopedAccessActivity.java
+++ b/src/com/android/documentsui/ScopedAccessActivity.java
@@ -16,367 +16,25 @@
package com.android.documentsui;
-import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME;
-import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME;
-
-import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED;
-import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED;
-import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_DENIED;
-import static com.android.documentsui.ScopedAccessMetrics
- .SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST;
-import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ERROR;
-import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_GRANTED;
-import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS;
+import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_DEPRECATED;
import static com.android.documentsui.ScopedAccessMetrics.logInvalidScopedAccessRequest;
-import static com.android.documentsui.ScopedAccessMetrics.logValidScopedAccessRequest;
-import static com.android.documentsui.base.SharedMinimal.DEBUG;
-import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT;
-import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName;
-import static com.android.documentsui.base.SharedMinimal.getUriPermission;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences
- .getScopedAccessPermissionStatus;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences
- .setScopedAccessPermissionStatus;
import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.GrantedUriPermission;
-import android.content.ContentProviderClient;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.net.Uri;
import android.os.Bundle;
-import android.os.Parcelable;
-import android.os.UserHandle;
import android.os.storage.StorageVolume;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.View;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.CompoundButton.OnCheckedChangeListener;
-import android.widget.TextView;
-
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.fragment.app.FragmentTransaction;
-
-import com.android.documentsui.base.Providers;
-
-import java.io.File;
/**
* Activity responsible for handling {@link StorageVolume#createAccessIntent(String)}.
+ *
+ * @deprecated This class handles the deprecated {@link StorageVolume#createAccessIntent(String)}.
*/
-public class ScopedAccessActivity extends AppCompatActivity {
- private static final String TAG = "ScopedAccessActivity";
- private static final String FM_TAG = "open_external_directory";
- private static final String EXTRA_FILE = "com.android.documentsui.FILE";
- private static final String EXTRA_APP_LABEL = "com.android.documentsui.APP_LABEL";
- private static final String EXTRA_VOLUME_LABEL = "com.android.documentsui.VOLUME_LABEL";
- private static final String EXTRA_VOLUME_UUID = "com.android.documentsui.VOLUME_UUID";
- private static final String EXTRA_IS_ROOT = "com.android.documentsui.IS_ROOT";
- private static final String EXTRA_IS_PRIMARY = "com.android.documentsui.IS_PRIMARY";
-
- private ContentProviderClient mExternalStorageClient;
-
+@Deprecated
+public class ScopedAccessActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- if (DEBUG) Log.d(TAG, "activity.onCreateDialog(): reusing instance");
- return;
- }
-
- final Intent intent = getIntent();
- if (intent == null) {
- if (DEBUG) Log.d(TAG, "missing intent");
- logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
- setResult(RESULT_CANCELED);
- finish();
- return;
- }
- final Parcelable storageVolume = intent.getParcelableExtra(EXTRA_STORAGE_VOLUME);
- if (!(storageVolume instanceof StorageVolume)) {
- if (DEBUG)
- Log.d(TAG, "extra " + EXTRA_STORAGE_VOLUME + " is not a StorageVolume: "
- + storageVolume);
- logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
- setResult(RESULT_CANCELED);
- finish();
- return;
- }
- String directoryName =
- getInternalDirectoryName(intent.getStringExtra(EXTRA_DIRECTORY_NAME));
- final StorageVolume volume = (StorageVolume) storageVolume;
- final String uuid = volume.isPrimary() ? null : volume.getUuid();
- if (getScopedAccessPermissionStatus(getApplicationContext(), getCallingPackage(),
- uuid, directoryName) == PERMISSION_NEVER_ASK) {
- logValidScopedAccessRequest(this, directoryName,
- SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED);
- setResult(RESULT_CANCELED);
- finish();
- return;
- }
-
- final int userId = UserHandle.myUserId();
- if (!showFragment(this, userId, volume, directoryName)) {
- setResult(RESULT_CANCELED);
- finish();
- return;
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (mExternalStorageClient != null) {
- mExternalStorageClient.close();
- }
- }
-
- /**
- * Validates the given path (volume + directory) and display the appropriate dialog asking the
- * user to grant access to it.
- */
- private static boolean showFragment(ScopedAccessActivity activity, int userId,
- StorageVolume storageVolume, String directoryName) {
- return getUriPermission(activity,
- activity.getExternalStorageClient(), storageVolume, directoryName, userId, true,
- (file, volumeLabel, isRoot, isPrimary, grantedUri, rootUri) -> {
- // Checks if the user has granted the permission already.
- final Intent intent = getIntentForExistingPermission(activity,
- activity.getCallingPackage(), grantedUri, rootUri);
- if (intent != null) {
- logValidScopedAccessRequest(activity, isRoot ? "." : directoryName,
- SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED);
- activity.setResult(RESULT_OK, intent);
- activity.finish();
- return true;
- }
-
- // Gets the package label.
- final String appLabel = getAppLabel(activity);
- if (appLabel == null) {
- // Error already logged.
- return false;
- }
-
- // Sets args that will be retrieve on onCreate()
- final Bundle args = new Bundle();
- args.putString(EXTRA_FILE, file.getAbsolutePath());
- args.putString(EXTRA_VOLUME_LABEL, volumeLabel);
- args.putString(EXTRA_VOLUME_UUID, isPrimary ? null : storageVolume.getUuid());
- args.putString(EXTRA_APP_LABEL, appLabel);
- args.putBoolean(EXTRA_IS_ROOT, isRoot);
- args.putBoolean(EXTRA_IS_PRIMARY, isPrimary);
-
- final FragmentManager fm = activity.getSupportFragmentManager();
- final FragmentTransaction ft = fm.beginTransaction();
- final ScopedAccessDialogFragment fragment = new ScopedAccessDialogFragment();
- fragment.setArguments(args);
- ft.add(fragment, FM_TAG);
- ft.commitAllowingStateLoss();
-
- return true;
- });
- }
-
- private static String getAppLabel(Activity activity) {
- final String packageName = activity.getCallingPackage();
- final PackageManager pm = activity.getPackageManager();
- try {
- return pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString();
- } catch (NameNotFoundException e) {
- logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR);
- Log.w(TAG, "Could not get label for package " + packageName);
- return null;
- }
- }
-
- private static Intent createGrantedUriPermissionsIntent(Context context,
- ContentProviderClient provider, File file) {
- final Uri uri = getUriPermission(context, provider, file);
- return createGrantedUriPermissionsIntent(uri);
- }
-
- private static Intent createGrantedUriPermissionsIntent(Uri uri) {
- final Intent intent = new Intent();
- intent.setData(uri);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
- return intent;
- }
-
- private static Intent getIntentForExistingPermission(Context context, String packageName,
- Uri grantedUri, Uri rootUri) {
- if (DEBUG) {
- Log.d(TAG, "checking if " + packageName + " already has permission for " + grantedUri
- + " or its root (" + rootUri + ")");
- }
- final ActivityManager am = context.getSystemService(ActivityManager.class);
- for (GrantedUriPermission uriPermission : am.getGrantedUriPermissions(packageName)
- .getList()) {
- final Uri uri = uriPermission.uri;
- if (uri == null) {
- Log.w(TAG, "null URI for " + uriPermission);
- continue;
- }
- if (uri.equals(grantedUri) || uri.equals(rootUri)) {
- if (DEBUG) Log.d(TAG, packageName + " already has permission: " + uriPermission);
- return createGrantedUriPermissionsIntent(grantedUri);
- }
- }
- if (DEBUG) Log.d(TAG, packageName + " does not have permission for " + grantedUri);
- return null;
- }
-
- public static class ScopedAccessDialogFragment extends DialogFragment {
-
- private File mFile;
- private String mVolumeUuid;
- private String mVolumeLabel;
- private String mAppLabel;
- private boolean mIsRoot;
- private boolean mIsPrimary;
- private CheckBox mDontAskAgain;
- private ScopedAccessActivity mActivity;
- private AlertDialog mDialog;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setRetainInstance(true);
- final Bundle args = getArguments();
- if (args != null) {
- mFile = new File(args.getString(EXTRA_FILE));
- mVolumeUuid = args.getString(EXTRA_VOLUME_UUID);
- mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL);
- mAppLabel = args.getString(EXTRA_APP_LABEL);
- mIsRoot = args.getBoolean(EXTRA_IS_ROOT);
- mIsPrimary= args.getBoolean(EXTRA_IS_PRIMARY);
- }
- mActivity = (ScopedAccessActivity) getActivity();
- }
-
- @Override
- public void onDestroyView() {
- // Workaround for https://code.google.com/p/android/issues/detail?id=17423
- if (mDialog != null && getRetainInstance()) {
- mDialog.setDismissMessage(null);
- }
- super.onDestroyView();
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- if (mDialog != null) {
- if (DEBUG) Log.d(TAG, "fragment.onCreateDialog(): reusing dialog");
- return mDialog;
- }
- if (mActivity != getActivity()) {
- // Sanity check.
- Log.wtf(TAG, "activity references don't match on onCreateDialog(): mActivity = "
- + mActivity + " , getActivity() = " + getActivity());
- mActivity = (ScopedAccessActivity) getActivity();
- }
- final String directory = mFile.getName();
- final String directoryName = mIsRoot ? DIRECTORY_ROOT : directory;
- final Context context = mActivity.getApplicationContext();
- final OnClickListener listener = new OnClickListener() {
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Intent intent = null;
- if (which == DialogInterface.BUTTON_POSITIVE) {
- intent = createGrantedUriPermissionsIntent(mActivity,
- mActivity.getExternalStorageClient(), mFile);
- }
- if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) {
- logValidScopedAccessRequest(mActivity, directoryName,
- SCOPED_DIRECTORY_ACCESS_DENIED);
- final boolean checked = mDontAskAgain.isChecked();
- if (checked) {
- logValidScopedAccessRequest(mActivity, directory,
- SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST);
- setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
- mVolumeUuid, directoryName, PERMISSION_NEVER_ASK);
- } else {
- setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
- mVolumeUuid, directoryName, PERMISSION_ASK_AGAIN);
- }
- mActivity.setResult(RESULT_CANCELED);
- } else {
- logValidScopedAccessRequest(mActivity, directory,
- SCOPED_DIRECTORY_ACCESS_GRANTED);
- mActivity.setResult(RESULT_OK, intent);
- }
- mActivity.finish();
- }
- };
-
- // It's ok pass null ViewRoot on AlertDialogs.
- final View view = View.inflate(mActivity, R.layout.dialog_open_scoped_directory, null);
- final CharSequence message;
- if (mIsRoot) {
- message = TextUtils.expandTemplate(getText(
- R.string.open_external_dialog_root_request), mAppLabel, mVolumeLabel);
- } else {
- message = TextUtils.expandTemplate(
- getText(mIsPrimary ? R.string.open_external_dialog_request_primary_volume
- : R.string.open_external_dialog_request),
- mAppLabel, directory, mVolumeLabel);
- }
- final TextView messageField = (TextView) view.findViewById(R.id.message);
- messageField.setText(message);
- mDialog = new AlertDialog.Builder(mActivity, R.style.Theme_AppCompat_Light_Dialog_Alert)
- .setView(view)
- .setPositiveButton(R.string.allow, listener)
- .setNegativeButton(R.string.deny, listener)
- .create();
-
- mDontAskAgain = (CheckBox) view.findViewById(R.id.do_not_ask_checkbox);
- if (getScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
- mVolumeUuid, directoryName) == PERMISSION_ASK_AGAIN) {
- mDontAskAgain.setVisibility(View.VISIBLE);
- mDontAskAgain.setOnCheckedChangeListener(new OnCheckedChangeListener() {
-
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!isChecked);
- }
- });
- }
-
- return mDialog;
- }
-
- @Override
- public void onCancel(DialogInterface dialog) {
- super.onCancel(dialog);
- final Activity activity = getActivity();
- logValidScopedAccessRequest(activity, mFile.getName(), SCOPED_DIRECTORY_ACCESS_DENIED);
- activity.setResult(RESULT_CANCELED);
- activity.finish();
- }
- }
-
- private synchronized ContentProviderClient getExternalStorageClient() {
- if (mExternalStorageClient == null) {
- mExternalStorageClient =
- getContentResolver().acquireContentProviderClient(Providers.AUTHORITY_STORAGE);
- }
- return mExternalStorageClient;
+ logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_DEPRECATED);
+ setResult(RESULT_CANCELED);
+ finish();
}
}
diff --git a/src/com/android/documentsui/ScopedAccessMetrics.java b/src/com/android/documentsui/ScopedAccessMetrics.java
index 1d94269d6..dd5fb6891 100644
--- a/src/com/android/documentsui/ScopedAccessMetrics.java
+++ b/src/com/android/documentsui/ScopedAccessMetrics.java
@@ -46,11 +46,14 @@ public final class ScopedAccessMetrics {
"docsui_scoped_directory_access_invalid_dir";
public static final String SCOPED_DIRECTORY_ACCESS_ERROR =
"docsui_scoped_directory_access_error";
+ public static final String SCOPED_DIRECTORY_ACCESS_DEPRECATED =
+ "docsui_scoped_directory_access_deprecated";
@StringDef(value = {
SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS,
SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY,
- SCOPED_DIRECTORY_ACCESS_ERROR
+ SCOPED_DIRECTORY_ACCESS_ERROR,
+ SCOPED_DIRECTORY_ACCESS_DEPRECATED
})
@Retention(RetentionPolicy.SOURCE)
public @interface InvalidScopedAccess{}
@@ -61,6 +64,7 @@ public final class ScopedAccessMetrics {
case SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS:
case SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY:
case SCOPED_DIRECTORY_ACCESS_ERROR:
+ case SCOPED_DIRECTORY_ACCESS_DEPRECATED:
logCount(context, type);
break;
default:
@@ -69,11 +73,11 @@ public final class ScopedAccessMetrics {
}
// Types for logValidScopedAccessRequest
- public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED = 0;
- public static final int SCOPED_DIRECTORY_ACCESS_GRANTED = 1;
- public static final int SCOPED_DIRECTORY_ACCESS_DENIED = 2;
- public static final int SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST = 3;
- public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED = 4;
+ @Deprecated public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED = 0;
+ @Deprecated public static final int SCOPED_DIRECTORY_ACCESS_GRANTED = 1;
+ @Deprecated public static final int SCOPED_DIRECTORY_ACCESS_DENIED = 2;
+ @Deprecated public static final int SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST = 3;
+ @Deprecated public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED = 4;
@IntDef(flag = true, value = {
SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED,
@@ -85,7 +89,7 @@ public final class ScopedAccessMetrics {
@Retention(RetentionPolicy.SOURCE)
public @interface ScopedAccessGrant {}
- public static void logValidScopedAccessRequest(Activity activity, String directory,
+ @Deprecated public static void logValidScopedAccessRequest(Activity activity, String directory,
@ScopedAccessGrant int type) {
int index = -1;
if (DIRECTORY_ROOT.equals(directory)) {
diff --git a/src/com/android/documentsui/ScopedAccessPackageReceiver.java b/src/com/android/documentsui/ScopedAccessPackageReceiver.java
deleted file mode 100644
index 995eedc16..000000000
--- a/src/com/android/documentsui/ScopedAccessPackageReceiver.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2017 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;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-
-import com.android.documentsui.prefs.ScopedAccessLocalPreferences;
-
-/**
- * Clean up {@link ScopedAccessLocalPreferences} when packages are removed.
- */
-public class ScopedAccessPackageReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- final Uri data = intent.getData();
- final String packageName = data == null ? null : data.getSchemeSpecificPart();
-
- if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) {
- if (packageName != null) {
- ScopedAccessLocalPreferences.clearPackagePreferences(context, packageName);
- }
- } else if (Intent.ACTION_PACKAGE_DATA_CLEARED.equals(action)) {
- if (packageName != null) {
- ScopedAccessLocalPreferences.clearPackagePreferences(context, packageName);
- }
- }
- }
-}
diff --git a/src/com/android/documentsui/ScopedAccessProvider.java b/src/com/android/documentsui/ScopedAccessProvider.java
deleted file mode 100644
index 96d0f3402..000000000
--- a/src/com/android/documentsui/ScopedAccessProvider.java
+++ /dev/null
@@ -1,600 +0,0 @@
-/*
- * 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.documentsui;
-
-import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_GRANTED;
-import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES;
-import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COLUMNS;
-import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COL_PACKAGE;
-import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS;
-import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COLUMNS;
-import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_DIRECTORY;
-import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_GRANTED;
-import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_PACKAGE;
-import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_VOLUME_UUID;
-import static android.os.Environment.isStandardDirectory;
-
-import static com.android.documentsui.base.SharedMinimal.DEBUG;
-import static com.android.documentsui.base.SharedMinimal.getExternalDirectoryName;
-import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName;
-import static com.android.documentsui.base.SharedMinimal.getUriPermission;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_GRANTED;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.clearScopedAccessPreferences;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getAllPackages;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getAllPermissions;
-import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.setScopedAccessPermissionStatus;
-import static androidx.core.util.Preconditions.checkArgument;
-
-import androidx.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.GrantedUriPermission;
-import android.content.ContentProvider;
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.net.Uri;
-import android.os.Environment;
-import android.os.UserHandle;
-import android.os.storage.StorageManager;
-import android.os.storage.StorageVolume;
-import android.provider.DocumentsContract;
-import android.util.ArraySet;
-import android.util.Log;
-
-import com.android.documentsui.base.Providers;
-import com.android.documentsui.prefs.ScopedAccessLocalPreferences.Permission;
-import com.android.internal.util.ArrayUtils;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-//TODO(b/72055774): update javadoc once implementation is finished
-/**
- * Provider used to manage scoped access directory permissions.
- *
- * <p>It fetches data from 2 sources:
- *
- * <ul>
- * <li>{@link com.android.documentsui.prefs.ScopedAccessLocalPreferences} for denied permissions.
- * <li>{@link ActivityManager} for allowed permissions.
- * </ul>
- *
- * <p>And returns the results in 2 tables:
- *
- * <ul>
- * <li>{@link #TABLE_PACKAGES}: read-only table with the name of all packages
- * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_PACKAGE}) that
- * had a scoped access directory permission granted or denied.
- * <li>{@link #TABLE_PERMISSIONS}: writable table with the name of all packages
- * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_PACKAGE}) that
- * had a scoped access directory
- * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_DIRECTORY})
- * permission for a volume (column
- * {@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_VOLUME_UUID}, which
- * contains the volume UUID or {@code null} if it's the primary partition) granted or denied
- * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_GRANTED}).
- * </ul>
- *
- * <p><b>Note:</b> the {@code query()} methods return all entries; it does not support selection or
- * projections.
- */
-// TODO(b/72055774): add unit tests
-public class ScopedAccessProvider extends ContentProvider {
-
- private static final String TAG = "ScopedAccessProvider";
- private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-
- private static final int URI_PACKAGES = 1;
- private static final int URI_PERMISSIONS = 2;
-
- public static final String AUTHORITY = "com.android.documentsui.scopedAccess";
-
- static {
- sMatcher.addURI(AUTHORITY, TABLE_PACKAGES + "/*", URI_PACKAGES);
- sMatcher.addURI(AUTHORITY, TABLE_PERMISSIONS + "/*", URI_PERMISSIONS);
- }
-
- @Override
- public boolean onCreate() {
- return true;
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
- String sortOrder) {
- if (DEBUG) {
- Log.v(TAG, "query(" + uri + "): proj=" + Arrays.toString(projection)
- + ", sel=" + selection);
- }
- switch (sMatcher.match(uri)) {
- case URI_PACKAGES:
- return getPackagesCursor();
- case URI_PERMISSIONS:
- if (ArrayUtils.isEmpty(selectionArgs)) {
- throw new UnsupportedOperationException("selections cannot be empty");
- }
- // For simplicity, we only support one package (which is what Settings is passing).
- if (selectionArgs.length > 1) {
- Log.w(TAG, "Using just first entry of " + Arrays.toString(selectionArgs));
- }
- return getPermissionsCursor(selectionArgs[0]);
- default:
- throw new UnsupportedOperationException("Unsupported Uri " + uri);
- }
- }
-
- private Cursor getPackagesCursor() {
- final Context context = getContext();
-
- // First, get the packages that were denied
- final Set<String> pkgs = getAllPackages(context);
-
- // Second, query AM to get all packages that have a permission.
- final ActivityManager am =
- (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-
- final List<GrantedUriPermission> amPkgs = am.getGrantedUriPermissions(null).getList();
- if (!amPkgs.isEmpty()) {
- amPkgs.forEach((perm) -> {
- if (isScopedAccessPermission(perm)) {
- pkgs.add(perm.packageName);
- }
- });
- }
-
- if (ArrayUtils.isEmpty(pkgs)) {
- if (DEBUG) Log.v(TAG, "getPackagesCursor(): nothing to do" );
- return null;
- }
-
- if (DEBUG) {
- Log.v(TAG, "getPackagesCursor(): denied=" + pkgs + ", granted=" + amPkgs);
- }
-
- // Finally, create the cursor
- final MatrixCursor cursor = new MatrixCursor(TABLE_PACKAGES_COLUMNS, pkgs.size());
- pkgs.forEach((pkg) -> cursor.addRow( new Object[] { pkg }));
- return cursor;
- }
-
- // TODO(b/72055774): need to unit tests to handle scenarios where the root permission of
- // a secondary volume mismatches a child permission (for example, child is allowed by root
- // is denied).
- private Cursor getPermissionsCursor(String packageName) {
- final Context context = getContext();
-
- // List of volumes that were granted by AM at the root level - in that case,
- // we can ignored individual grants from AM or denials from our preferences
- final Set<String> grantedVolumes = new ArraySet<>();
-
- // List of directories (mapped by volume uuid) that were granted by AM so they can be
- // ignored if also found on our preferences
- final Map<String, Set<String>> grantedDirsByUuid = new HashMap<>();
-
- // Cursor rows
- final List<Object[]> permissions = new ArrayList<>();
-
- // First, query AM to get all packages that have a permission.
- final ActivityManager am =
- (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
- final List<GrantedUriPermission> uriPermissions =
- am.getGrantedUriPermissions(packageName).getList();
- if (DEBUG) {
- Log.v(TAG, "am returned =" + uriPermissions);
- }
- setGrantedPermissions(packageName, uriPermissions, permissions, grantedVolumes,
- grantedDirsByUuid);
-
- // Now gets the packages that were denied
- final List<Permission> rawPermissions = getAllPermissions(context);
-
- if (DEBUG) {
- Log.v(TAG, "rawPermissions: " + rawPermissions);
- }
-
- // Merge the permissions granted by AM with the denied permissions saved on our preferences.
- for (Permission rawPermission : rawPermissions) {
- if (!packageName.equals(rawPermission.pkg)) {
- if (DEBUG) {
- Log.v(TAG,
- "ignoring " + rawPermission + " because package is not " + packageName);
- }
- continue;
- }
- if (rawPermission.status != PERMISSION_NEVER_ASK
- && rawPermission.status != PERMISSION_ASK_AGAIN) {
- // We only care for status where the user denied a request.
- if (DEBUG) {
- Log.v(TAG, "ignoring " + rawPermission + " because of its status");
- }
- continue;
- }
- if (grantedVolumes.contains(rawPermission.uuid)) {
- if (DEBUG) {
- Log.v(TAG, "ignoring " + rawPermission + " because whole volume is granted");
- }
- continue;
- }
- final Set<String> grantedDirs = grantedDirsByUuid.get(rawPermission.uuid);
- if (grantedDirs != null
- && grantedDirs.contains(rawPermission.directory)) {
- Log.w(TAG, "ignoring " + rawPermission + " because it was granted already");
- continue;
- }
- permissions.add(new Object[] {
- packageName, rawPermission.uuid,
- getExternalDirectoryName(rawPermission.directory), 0
- });
- }
-
- if (DEBUG) {
- Log.v(TAG, "total permissions: " + permissions.size());
- }
-
- // Then create the cursor
- final MatrixCursor cursor = new MatrixCursor(TABLE_PERMISSIONS_COLUMNS, permissions.size());
- permissions.forEach((row) -> cursor.addRow(row));
- return cursor;
- }
-
- /**
- * Converts the permissions returned by AM and add it to 3 buckets ({@code permissions},
- * {@code grantedVolumes}, and {@code grantedDirsByUuid}).
- *
- * @param packageName name of package that the permissions were granted to.
- * @param uriPermissions permissions returend by AM
- * @param permissions list of permissions that can be converted to a {@link #TABLE_PERMISSIONS}
- * row.
- * @param grantedVolumes volume uuids that were granted full access.
- * @param grantedDirsByUuid directories that were granted individual acces (key is volume uuid,
- * value is list of directories).
- */
- private void setGrantedPermissions(String packageName, List<GrantedUriPermission> uriPermissions,
- List<Object[]> permissions, Set<String> grantedVolumes,
- Map<String, Set<String>> grantedDirsByUuid) {
- final List<Permission> grantedPermissions = parseGrantedPermissions(uriPermissions);
-
- for (Permission p : grantedPermissions) {
- // First check if it's for the full volume
- if (p.directory == null) {
- if (p.uuid == null) {
- // Should never happen - the Scoped Directory Access API does not allow it.
- Log.w(TAG, "ignoring entry whose uuid and directory is null");
- continue;
- }
- grantedVolumes.add(p.uuid);
- } else {
- if (!ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, p.directory)) {
- if (DEBUG) Log.v(TAG, "Ignoring non-standard directory on " + p);
- continue;
- }
-
- Set<String> dirs = grantedDirsByUuid.get(p.uuid);
- if (dirs == null) {
- // Life would be so much easier if Android had MultiMaps...
- dirs = new HashSet<>(1);
- grantedDirsByUuid.put(p.uuid, dirs);
- }
- dirs.add(p.directory);
- }
- }
-
- if (DEBUG) {
- Log.v(TAG, "grantedVolumes=" + grantedVolumes
- + ", grantedDirectories=" + grantedDirsByUuid);
- }
- // Add granted permissions to full volumes.
- grantedVolumes.forEach((uuid) -> permissions.add(new Object[] {
- packageName, uuid, /* dir= */ null, 1
- }));
-
- // Add granted permissions to individual directories
- grantedDirsByUuid.forEach((uuid, dirs) -> {
- if (grantedVolumes.contains(uuid)) {
- Log.w(TAG, "Ignoring individual grants to " + uuid + ": " + dirs);
- } else {
- dirs.forEach((dir) -> permissions.add(new Object[] {packageName, uuid, dir, 1}));
- }
- });
- }
-
- /**
- * Converts the permissions returned by AM to our own format.
- */
- private List<Permission> parseGrantedPermissions(List<GrantedUriPermission> uriPermissions) {
- final List<Permission> permissions = new ArrayList<>(uriPermissions.size());
- // TODO(b/72055774): we should query AUTHORITY_STORAGE or call DocumentsContract instead of
- // hardcoding the logic here.
- for (GrantedUriPermission uriPermission : uriPermissions) {
- final Uri uri = uriPermission.uri;
- final String authority = uri.getAuthority();
- if (!Providers.AUTHORITY_STORAGE.equals(authority)) {
- Log.w(TAG, "Wrong authority on " + uri);
- continue;
- }
- final List<String> pathSegments = uri.getPathSegments();
- if (pathSegments.size() < 2) {
- Log.w(TAG, "wrong path segments on " + uri);
- continue;
- }
- // TODO(b/72055774): make PATH_TREE private again if not used anymore
- if (!DocumentsContract.PATH_TREE.equals(pathSegments.get(0))) {
- Log.w(TAG, "wrong path tree on " + uri);
- continue;
- }
-
- final String[] uuidAndDir = pathSegments.get(1).split(":");
- // uuid and dir are either UUID:DIR (for scoped directory) or UUID: (for full volume)
- if (uuidAndDir.length != 1 && uuidAndDir.length != 2) {
- Log.w(TAG, "could not parse uuid and directory on " + uri);
- continue;
- }
- // TODO(b/72055774): to make things uglier, the Documents directory in the primary
- // storage is a special case as its URI is "$ROOT_ID_HOME", instead of
- // "${ROOT_ID_DEVICE}/Documents. This is another reason to move this logic to the
- // provider...
- final String uuid, dir;
- if (Providers.ROOT_ID_HOME.equals(uuidAndDir[0])) {
- uuid = null;
- dir = Environment.DIRECTORY_DOCUMENTS;
- } else {
- uuid = Providers.ROOT_ID_DEVICE.equals(uuidAndDir[0])
- ? null // primary
- : uuidAndDir[0]; // external volume
- dir = uuidAndDir.length == 1 ? null : uuidAndDir[1];
- }
- permissions
- .add(new Permission(uriPermission.packageName, uuid, dir, PERMISSION_GRANTED));
- }
- return permissions;
- }
-
- private boolean isScopedAccessPermission(GrantedUriPermission uriPermission) {
- // TODO(b/72055774): we should query AUTHORITY_STORAGE or call DocumentsContract instead of
- // hardcoding the logic here.
- final Uri uri = uriPermission.uri;
- final String authority = uri.getAuthority();
- if (!Providers.AUTHORITY_STORAGE.equals(authority)) {
- return false;
- }
- final List<String> pathSegments = uri.getPathSegments();
- if (pathSegments.size() < 2) {
- return false;
- }
- // TODO(b/72055774): make PATH_TREE private again if not used anymore
- if (!DocumentsContract.PATH_TREE.equals(pathSegments.get(0))) {
- return false;
- }
-
- final String[] uuidAndDir = pathSegments.get(1).split(":");
- // uuid and dir are either UUID:DIR (for scoped directory) or UUID: (for full volume)
- if (uuidAndDir.length != 1 && uuidAndDir.length != 2) {
- return false;
- }
- final String uuid, dir;
- if (Providers.ROOT_ID_HOME.equals(uuidAndDir[0])) {
- uuid = null;
- dir = Environment.DIRECTORY_DOCUMENTS;
- } else {
- uuid = Providers.ROOT_ID_DEVICE.equals(uuidAndDir[0])
- ? null // primary
- : uuidAndDir[0]; // external volume
- dir = uuidAndDir.length == 1 ? null : uuidAndDir[1];
- }
- if ((dir == null && uuid != null) || !Environment.isStandardDirectory(dir)) {
- return false;
- }
-
- return true;
- }
-
- @Override
- public String getType(Uri uri) {
- return null;
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues values) {
- throw new UnsupportedOperationException("insert(): unsupported " + uri);
- }
-
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- if (sMatcher.match(uri) != URI_PERMISSIONS) {
- throw new UnsupportedOperationException("delete(): unsupported " + uri);
- }
-
- if (DEBUG) {
- Log.v(TAG, "delete(" + uri + "): " + Arrays.toString(selectionArgs));
- }
-
- // TODO(b/72055774): add unit tests for invalid input
- checkArgument(selectionArgs != null && selectionArgs.length == 1,
- "Must have exactly 1 args: package_name" + Arrays.toString(selectionArgs));
- final String packageName = selectionArgs[0];
-
- // Delete just our preferences - the URI permissions is handled externally
- // TODO(b/72055774): move logic to revoke permissions here, so AppStorageSettings does
- // not need to call am.clearGrantedUriPermissions(packageName) (then we could remove that
- // method from ActivityManager)
- return clearScopedAccessPreferences(getContext(), packageName);
- }
-
- @Override
- public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- if (sMatcher.match(uri) != URI_PERMISSIONS) {
- throw new UnsupportedOperationException("update(): unsupported " + uri);
- }
-
- if (DEBUG) {
- Log.v(TAG, "update(" + uri + "): " + Arrays.toString(selectionArgs) + " = " + values);
- }
-
- // TODO(b/72055774): add unit tests for invalid input
- checkArgument(selectionArgs != null && selectionArgs.length == 3,
- "Must have exactly 3 args: package_name, (nullable) uuid, (nullable) directory: "
- + Arrays.toString(selectionArgs));
- final String packageName = selectionArgs[0];
- final String uuid = selectionArgs[1];
- final String dir = selectionArgs[2];
- final boolean granted = values.getAsBoolean(COL_GRANTED);
-
- // First update the effective URI permission ...
- if (!persistUriPermission(packageName, uuid, dir, granted)) {
- // Failed - nothing left to do...
- return 0;
- }
-
- // ...then our preferences.
- setScopedAccessPermissionStatus(getContext(), packageName, uuid,
- getInternalDirectoryName(dir), granted ? PERMISSION_GRANTED : PERMISSION_NEVER_ASK);
- return 1;
- }
-
- /**
- * Calls AM to persist a URI.
- *
- * @return whether the call succeeded.
- */
- private boolean persistUriPermission(String packageName, @Nullable String uuid,
- @Nullable String directory, boolean granted) {
- final Context context = getContext();
-
- final ContentProviderClient storageClient = context.getContentResolver()
- .acquireContentProviderClient(Providers.AUTHORITY_STORAGE);
-
- final StorageManager sm = context.getSystemService(StorageManager.class);
-
- StorageVolume volume = null;
- if (uuid == null) {
- if (directory == null) {
- Log.w(TAG, "cannot grant full access to the primary volume");
- return false;
- }
- volume = sm.getPrimaryStorageVolume();
- } else {
- for (StorageVolume candidate : sm.getVolumeList()) {
- if (uuid.equals(candidate.getUuid())) {
- volume = candidate;
- break;
- }
- }
- if (volume == null) {
- Log.w(TAG, "didn't find volume for UUID=" + uuid);
- return false;
- }
- if (directory != null && !isStandardDirectory(directory)) {
- Log.w(TAG, "not a scoped directory: " + directory);
- return false;
- }
- }
-
- return getUriPermission(context, storageClient, volume, getInternalDirectoryName(directory),
- UserHandle.getCallingUserId(), /* logMetrics= */ false,
- (file, volumeLabel, isRoot, isPrimary, grantedUri, rootUri) -> {
- updatePermission(context, grantedUri, packageName, granted);
- return true;
- });
- }
-
- private void updatePermission(Context context, Uri grantedUri, String toPackage,
- boolean granted) {
- final int persistFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
- final int grantFlags = persistFlags
- | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
-
- final ContentResolver cr = context.getContentResolver();
- if (granted) {
- context.grantUriPermission(toPackage, grantedUri, grantFlags);
- cr.takePersistableUriPermission(toPackage, grantedUri, persistFlags);
- } else {
- context.revokeUriPermission(toPackage, grantedUri, grantFlags);
- // There's no need to release after revoking
- }
- }
-
- @Override
- public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- final String prefix = " ";
-
- final List<String> packages = new ArrayList<>();
- pw.print("Packages: ");
- try (Cursor cursor = getPackagesCursor()) {
- if (cursor == null || cursor.getCount() == 0) {
- pw.println("N/A");
- } else {
- pw.println(cursor.getCount());
- while (cursor.moveToNext()) {
- final String pkg = cursor.getString(TABLE_PACKAGES_COL_PACKAGE);
- packages.add(pkg);
- pw.print(prefix);
- pw.println(pkg);
- }
- }
- }
-
- pw.print("Permissions: ");
- for (int i = 0; i < packages.size(); i++) {
- final String pkg = packages.get(i);
- try (Cursor cursor = getPermissionsCursor(pkg)) {
- if (cursor == null) {
- pw.println("N/A");
- } else {
- pw.println(cursor.getCount());
- while (cursor.moveToNext()) {
- pw.print(prefix); pw.print(cursor.getString(TABLE_PERMISSIONS_COL_PACKAGE));
- pw.print('/');
- final String uuid = cursor.getString(TABLE_PERMISSIONS_COL_VOLUME_UUID);
- if (uuid != null) {
- pw.print(uuid); pw.print('>');
- }
- pw.print(cursor.getString(TABLE_PERMISSIONS_COL_DIRECTORY));
- pw.print(": "); pw.println(cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1);
- }
- }
- }
- }
-
- pw.print("Raw permissions: ");
- final List<Permission> rawPermissions = getAllPermissions(getContext());
- if (rawPermissions.isEmpty()) {
- pw.println("N/A");
- } else {
- final int size = rawPermissions.size();
- pw.println(size);
- for (int i = 0; i < size; i++) {
- final Permission permission = rawPermissions.get(i);
- pw.print(prefix); pw.println(permission);
- }
- }
- }
-}
diff --git a/src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java b/src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java
index 08cbf1a62..0bdd484dd 100644
--- a/src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java
+++ b/src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java
@@ -15,34 +15,16 @@
*/
package com.android.documentsui.prefs;
-import static com.android.documentsui.base.SharedMinimal.DEBUG;
-import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT;
-import static androidx.core.util.Preconditions.checkArgument;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
-import android.os.UserHandle;
import android.preference.PreferenceManager;
-import android.text.TextUtils;
-import android.util.ArraySet;
-import android.util.Log;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
/**
* Methods for accessing the local preferences with regards to scoped directory access.
+ * TODO(b/111892460): Delete this class after Q is released.
*/
-//TODO(b/72055774): add unit tests
+@Deprecated
public class ScopedAccessLocalPreferences {
private static final String TAG = "ScopedAccessLocalPreferences";
@@ -51,188 +33,21 @@ public class ScopedAccessLocalPreferences {
return PreferenceManager.getDefaultSharedPreferences(context);
}
- public static final int PERMISSION_ASK = 0;
- public static final int PERMISSION_ASK_AGAIN = 1;
- public static final int PERMISSION_NEVER_ASK = -1;
- // NOTE: this status is not used on preferences, but on permissions granted by AM
- public static final int PERMISSION_GRANTED = 2;
-
- @IntDef(flag = true, value = {
- PERMISSION_ASK,
- PERMISSION_ASK_AGAIN,
- PERMISSION_NEVER_ASK,
- PERMISSION_GRANTED
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface PermissionStatus {}
-
- private static final String KEY_REGEX = "^.+\\|(.+)\\|(.*)\\|(.+)$";
- private static final Pattern KEY_PATTERN = Pattern.compile(KEY_REGEX);
-
- /**
- * Methods below are used to keep track of denied user requests on scoped directory access so
- * the dialog is not offered when user checked the 'Do not ask again' box
- *
- * <p>It uses a shared preferences, whose key is:
- * <ol>
- * <li>{@code USER_ID|PACKAGE_NAME|VOLUME_UUID|DIRECTORY} for storage volumes that have a UUID
- * (typically physical volumes like SD cards).
- * <li>{@code USER_ID|PACKAGE_NAME||DIRECTORY} for storage volumes that do not have a UUID
- * (typically the emulated volume used for primary storage
- * </ol>
- */
- public static @PermissionStatus int getScopedAccessPermissionStatus(Context context,
- String packageName, @Nullable String uuid, String directory) {
- final String key = getScopedAccessDenialsKey(packageName, uuid, directory);
- return getPrefs(context).getInt(key, PERMISSION_ASK);
- }
-
- public static void setScopedAccessPermissionStatus(Context context, String packageName,
- @Nullable String uuid, String directory, @PermissionStatus int status) {
- checkArgument(!TextUtils.isEmpty(directory),
- "Cannot pass empty directory - did you mean " + DIRECTORY_ROOT + "?");
- final String key = getScopedAccessDenialsKey(packageName, uuid, directory);
- if (DEBUG) {
- Log.d(TAG, "Setting permission of " + packageName + ":" + uuid + ":" + directory
- + " to " + statusAsString(status));
- }
-
- getPrefs(context).edit().putInt(key, status).apply();
- }
-
- public static int clearScopedAccessPreferences(Context context, String packageName) {
- final String keySubstring = "|" + packageName + "|";
+ /** Clears all scoped directory access preferences. */
+ public static void clearScopedAccessPreferences(Context context) {
+ final String keySubstring = "|";
final SharedPreferences prefs = getPrefs(context);
Editor editor = null;
- int removed = 0;
for (final String key : prefs.getAll().keySet()) {
if (key.contains(keySubstring)) {
if (editor == null) {
editor = prefs.edit();
}
editor.remove(key);
- removed ++;
}
}
if (editor != null) {
editor.apply();
}
- return removed;
- }
-
- private static String getScopedAccessDenialsKey(String packageName, @Nullable String uuid,
- String directory) {
- final int userId = UserHandle.myUserId();
- return uuid == null
- ? userId + "|" + packageName + "||" + directory
- : userId + "|" + packageName + "|" + uuid + "|" + directory;
- }
-
- /**
- * Clears all preferences associated with a given package.
- *
- * <p>Typically called when a package is removed or when user asked to clear its data.
- */
- public static void clearPackagePreferences(Context context, String packageName) {
- ScopedAccessLocalPreferences.clearScopedAccessPreferences(context, packageName);
- }
-
- /**
- * Gets all packages that have entries in the preferences
- */
- public static Set<String> getAllPackages(Context context) {
- final SharedPreferences prefs = getPrefs(context);
-
- final ArraySet<String> pkgs = new ArraySet<>();
- for (Entry<String, ?> pref : prefs.getAll().entrySet()) {
- final String key = pref.getKey();
- final String pkg = getPackage(key);
- if (pkg == null) {
- Log.w(TAG, "getAllPackages(): error parsing pref '" + key + "'");
- continue;
- }
- pkgs.add(pkg);
- }
- return pkgs;
- }
-
- /**
- * Gets all permissions.
- */
- public static List<Permission> getAllPermissions(Context context) {
- final SharedPreferences prefs = getPrefs(context);
- final ArrayList<Permission> permissions = new ArrayList<>();
-
- for (Entry<String, ?> pref : prefs.getAll().entrySet()) {
- final String key = pref.getKey();
- final Object value = pref.getValue();
- final Integer status;
- try {
- status = (Integer) value;
- } catch (Exception e) {
- Log.w(TAG, "error gettting value for key '" + key + "': " + value);
- continue;
- }
- final Permission permission = getPermission(key, status);
- if (permission != null) {
- permissions.add(permission);
- }
- }
-
- return permissions;
- }
-
- public static String statusAsString(@PermissionStatus int status) {
- switch (status) {
- case PERMISSION_ASK:
- return "PERMISSION_ASK";
- case PERMISSION_ASK_AGAIN:
- return "PERMISSION_ASK_AGAIN";
- case PERMISSION_NEVER_ASK:
- return "PERMISSION_NEVER_ASK";
- case PERMISSION_GRANTED:
- return "PERMISSION_GRANTED";
- default:
- return "UNKNOWN";
- }
- }
-
- @Nullable
- private static String getPackage(String key) {
- final Matcher matcher = KEY_PATTERN.matcher(key);
- return matcher.matches() ? matcher.group(1) : null;
- }
-
- private static Permission getPermission(String key, Integer status) {
- final Matcher matcher = KEY_PATTERN.matcher(key);
- if (!matcher.matches()) return null;
-
- final String pkg = matcher.group(1);
- final String uuid = matcher.group(2);
- final String directory = matcher.group(3);
-
- return new Permission(pkg, uuid, directory, status);
- }
-
- public static final class Permission {
- public final String pkg;
-
- @Nullable
- public final String uuid;
- public final String directory;
- public final int status;
-
- public Permission(String pkg, String uuid, String directory, Integer status) {
- this.pkg = pkg;
- this.uuid = TextUtils.isEmpty(uuid) ? null : uuid;
- this.directory = directory;
- this.status = status.intValue();
- }
-
- @Override
- public String toString() {
- return "Permission: [pkg=" + pkg + ", uuid=" + uuid + ", dir=" + directory + ", status="
- + statusAsString(status) + " (" + status + ")]";
- }
}
}