diff options
author | 2018-10-23 14:22:48 -0400 | |
---|---|---|
committer | 2018-11-02 15:12:55 -0400 | |
commit | ced7a45a757c48271a9cf630f64ae7cdfa3c6d7d (patch) | |
tree | e9b4fec9ada065124359e123d7933c36e8474f49 | |
parent | 8d8d92f38f2a92cc914b3e59648f3eee7ce9af47 (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.bp | 2 | ||||
-rw-r--r-- | AndroidManifest.xml | 7 | ||||
-rw-r--r-- | minimal/AndroidManifest.xml | 15 | ||||
-rw-r--r-- | minimal/res/layout/dialog_open_scoped_directory.xml | 44 | ||||
-rw-r--r-- | minimal/res/values/strings.xml | 2 | ||||
-rw-r--r-- | res/layout/dialog_open_scoped_directory.xml | 44 | ||||
-rw-r--r-- | res/values/strings.xml | 2 | ||||
-rw-r--r-- | src/com/android/documentsui/DocumentsApplication.java | 2 | ||||
-rw-r--r-- | src/com/android/documentsui/PackageReceiver.java | 8 | ||||
-rw-r--r-- | src/com/android/documentsui/ScopedAccessActivity.java | 358 | ||||
-rw-r--r-- | src/com/android/documentsui/ScopedAccessMetrics.java | 18 | ||||
-rw-r--r-- | src/com/android/documentsui/ScopedAccessPackageReceiver.java | 46 | ||||
-rw-r--r-- | src/com/android/documentsui/ScopedAccessProvider.java | 600 | ||||
-rw-r--r-- | src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java | 195 |
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 + ")]"; - } } } |