From adccb9950ee3357a7c35283235c2b01500c43aec Mon Sep 17 00:00:00 2001 From: Felipe Leme Date: Wed, 9 Mar 2016 17:40:49 -0800 Subject: [PATCH] Add a "Do not ask again" checkbox. When an app request access to a scoped directory which the user already denied access, it will display a "Do not ask again" checkbox; if the user checks that option, further requests will be automatically rejected. The history of denials is stored in the shared property file. The UI is not polished yet, the style will be fixed in a future change. BUG: 26750152 Change-Id: I181923adfb6a1c7c1c17e305d6838314280417fc --- .../layout/dialog_open_scoped_directory.xml | 45 ++++++++ packages/DocumentsUI/res/values/strings.xml | 6 +- .../android/documentsui/LocalPreferences.java | 74 ++++++++++-- .../src/com/android/documentsui/Metrics.java | 43 ++++--- .../OpenExternalDirectoryActivity.java | 109 +++++++++++++++--- proto/src/metrics_constants.proto | 16 +++ 6 files changed, 254 insertions(+), 39 deletions(-) create mode 100644 packages/DocumentsUI/res/layout/dialog_open_scoped_directory.xml diff --git a/packages/DocumentsUI/res/layout/dialog_open_scoped_directory.xml b/packages/DocumentsUI/res/layout/dialog_open_scoped_directory.xml new file mode 100644 index 0000000000000..d4a450bf2f345 --- /dev/null +++ b/packages/DocumentsUI/res/layout/dialog_open_scoped_directory.xml @@ -0,0 +1,45 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml index e2d1870cb122d..e7406e68302de 100644 --- a/packages/DocumentsUI/res/values/strings.xml +++ b/packages/DocumentsUI/res/values/strings.xml @@ -200,10 +200,12 @@ during a copy. [CHAR LIMIT=48] --> Some files were converted - + Grant ^1 - access to ^2 folder on + access to ^2 directory on ^3? + + Don\'t ask again Allow diff --git a/packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java b/packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java index c7c61c3189ac2..8c4859f52a02b 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java +++ b/packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java @@ -16,10 +16,20 @@ package com.android.documentsui; +import static com.android.documentsui.Shared.DEBUG; +import static com.android.documentsui.Shared.TAG; import static com.android.documentsui.State.MODE_UNKNOWN; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import android.annotation.IntDef; +import android.annotation.Nullable; import android.content.Context; +import android.content.SharedPreferences; +import android.os.UserHandle; import android.preference.PreferenceManager; +import android.util.Log; import com.android.documentsui.State.ViewMode; import com.android.documentsui.model.RootInfo; @@ -29,29 +39,73 @@ public class LocalPreferences { private static final String ROOT_VIEW_MODE_PREFIX = "rootViewMode-"; public static boolean getDisplayFileSize(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(KEY_FILE_SIZE, false); + return getPrefs(context).getBoolean(KEY_FILE_SIZE, false); } - public static @ViewMode int getViewMode( - Context context, RootInfo root, @ViewMode int fallback) { - return PreferenceManager.getDefaultSharedPreferences(context) - .getInt(createKey(root), fallback); + public static @ViewMode int getViewMode(Context context, RootInfo root, + @ViewMode int fallback) { + return getPrefs(context).getInt(createKey(root), fallback); } public static void setDisplayFileSize(Context context, boolean display) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putBoolean(KEY_FILE_SIZE, display).apply(); + getPrefs(context).edit().putBoolean(KEY_FILE_SIZE, display).apply(); } public static void setViewMode(Context context, RootInfo root, @ViewMode int viewMode) { assert(viewMode != MODE_UNKNOWN); - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putInt(createKey(root), viewMode).apply(); + getPrefs(context).edit().putInt(createKey(root), viewMode).apply(); + } + + private static SharedPreferences getPrefs(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context); } private static String createKey(RootInfo root) { return ROOT_VIEW_MODE_PREFIX + root.authority + root.rootId; } + + public static final int PERMISSION_ASK = 0; + public static final int PERMISSION_ASK_AGAIN = 1; + public static final int PERMISSION_NEVER_ASK = -1; + + @IntDef(flag = true, value = { + PERMISSION_ASK, + PERMISSION_ASK_AGAIN, + PERMISSION_NEVER_ASK, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PermissionStatus {} + + /** + * 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 + * + *

It uses a shared preferences, whose key is: + *

    + *
  1. {@code USER_ID|PACKAGE_NAME|VOLUME_UUID|DIRECTORY} for storage volumes that have a UUID + * (typically physical volumes like SD cards). + *
  2. {@code USER_ID|PACKAGE_NAME||DIRECTORY} for storage volumes that do not have a UUID + * (typically the emulated volume used for primary storage + *
+ */ + 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); + } + + static void setScopedAccessPermissionStatus(Context context, String packageName, + @Nullable String uuid, String directory, @PermissionStatus int status) { + final String key = getScopedAccessDenialsKey(packageName, uuid, directory); + getPrefs(context).edit().putInt(key, status).apply(); + } + + private static String getScopedAccessDenialsKey(String packageName, String uuid, + String directory) { + final int userId = UserHandle.myUserId(); + return uuid == null + ? userId + "|" + packageName + "||" + directory + : userId + "|" + packageName + "|" + uuid + "|" + directory; + } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/Metrics.java b/packages/DocumentsUI/src/com/android/documentsui/Metrics.java index afd308cc97a77..deef1c278e633 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/Metrics.java +++ b/packages/DocumentsUI/src/com/android/documentsui/Metrics.java @@ -411,11 +411,15 @@ public final class Metrics { 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; @IntDef(flag = true, value = { SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED, SCOPED_DIRECTORY_ACCESS_GRANTED, - SCOPED_DIRECTORY_ACCESS_DENIED + SCOPED_DIRECTORY_ACCESS_DENIED, + SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST, + SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED }) @Retention(RetentionPolicy.SOURCE) public @interface ScopedAccessGrant {} @@ -432,23 +436,34 @@ public final class Metrics { final String packageName = activity.getCallingPackage(); switch (type) { case SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED: - MetricsLogger.action(activity, - MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_PACKAGE, - packageName); - MetricsLogger.action(activity, - MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_FOLDER, index); + MetricsLogger.action(activity, MetricsEvent + .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_PACKAGE, packageName); + MetricsLogger.action(activity, MetricsEvent + .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_GRANTED: - MetricsLogger.action(activity, - MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_PACKAGE, packageName); - MetricsLogger.action(activity, - MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_FOLDER, index); + MetricsLogger.action(activity, MetricsEvent + .ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_PACKAGE, packageName); + MetricsLogger.action(activity, MetricsEvent + .ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_DENIED: - MetricsLogger.action(activity, - MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_PACKAGE, packageName); - MetricsLogger.action(activity, - MetricsEvent.ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_FOLDER, index); + MetricsLogger.action(activity, MetricsEvent + .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_PACKAGE, packageName); + MetricsLogger.action(activity, MetricsEvent + .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_FOLDER, index); + break; + case SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST: + MetricsLogger.action(activity, MetricsEvent + .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_PACKAGE, packageName); + MetricsLogger.action(activity, MetricsEvent + .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_FOLDER, index); + break; + case SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED: + MetricsLogger.action(activity, MetricsEvent + .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_PACKAGE, packageName); + MetricsLogger.action(activity, MetricsEvent + .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_FOLDER, index); break; default: Log.wtf(TAG, "invalid ScopedAccessGrant: " + type); diff --git a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java index dc529ceb18ece..f30ab238f5529 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java @@ -20,16 +20,24 @@ import static android.os.Environment.isStandardDirectory; import static android.os.Environment.STANDARD_DIRECTORIES; import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME; import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME; -import static com.android.documentsui.Shared.DEBUG; -import static com.android.documentsui.Metrics.logInvalidScopedAccessRequest; -import static com.android.documentsui.Metrics.logValidScopedAccessRequest; +import static com.android.documentsui.LocalPreferences.getScopedAccessPermissionStatus; +import static com.android.documentsui.LocalPreferences.PERMISSION_ASK; +import static com.android.documentsui.LocalPreferences.PERMISSION_ASK_AGAIN; +import static com.android.documentsui.LocalPreferences.PERMISSION_NEVER_ASK; +import static com.android.documentsui.LocalPreferences.setScopedAccessPermissionStatus; +import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED; +import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_ERROR; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_GRANTED; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS; import static com.android.documentsui.Metrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY; +import static com.android.documentsui.Metrics.logInvalidScopedAccessRequest; +import static com.android.documentsui.Metrics.logValidScopedAccessRequest; +import static com.android.documentsui.Shared.DEBUG; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityManager; import android.app.AlertDialog; @@ -38,7 +46,6 @@ import android.app.DialogFragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.ContentProviderClient; -import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; @@ -57,6 +64,11 @@ import android.os.storage.VolumeInfo; import android.provider.DocumentsContract; 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 java.io.File; import java.io.IOException; @@ -72,12 +84,17 @@ public class OpenExternalDirectoryActivity extends Activity { 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 ContentProviderClient mExternalStorageClient; @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) { @@ -105,9 +122,18 @@ public class OpenExternalDirectoryActivity extends Activity { finish(); return; } + final StorageVolume volume = (StorageVolume) storageVolume; + if (getScopedAccessPermissionStatus(getApplicationContext(), getCallingPackage(), + volume.getUuid(), 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, (StorageVolume) storageVolume, directoryName)) { + if (!showFragment(this, userId, volume, directoryName)) { setResult(RESULT_CANCELED); finish(); return; @@ -157,6 +183,7 @@ public class OpenExternalDirectoryActivity extends Activity { // Gets volume label and converted path. String volumeLabel = null; + String volumeUuid = null; final List volumes = sm.getVolumes(); if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size()); for (VolumeInfo volume : volumes) { @@ -166,6 +193,7 @@ public class OpenExternalDirectoryActivity extends Activity { if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot); file = new File(internalRoot, directory); volumeLabel = sm.getBestVolumeDescription(volume); + volumeUuid = volume.getFsUuid(); break; } } @@ -197,6 +225,7 @@ public class OpenExternalDirectoryActivity extends Activity { final Bundle args = new Bundle(); args.putString(EXTRA_FILE, file.getAbsolutePath()); args.putString(EXTRA_VOLUME_LABEL, volumeLabel); + args.putString(EXTRA_VOLUME_UUID, volumeUuid); args.putString(EXTRA_APP_LABEL, appLabel); final FragmentManager fm = activity.getFragmentManager(); @@ -303,26 +332,50 @@ public class OpenExternalDirectoryActivity extends Activity { public static class OpenExternalDirectoryDialogFragment extends DialogFragment { private File mFile; + private String mVolumeUuid; private String mVolumeLabel; private String mAppLabel; + private CheckBox mDontAskAgain; private OpenExternalDirectoryActivity 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); } mActivity = (OpenExternalDirectoryActivity) 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 = (OpenExternalDirectoryActivity) getActivity(); + } final String directory = mFile.getName(); - final Activity activity = getActivity(); + final Context context = mActivity.getApplicationContext(); final OnClickListener listener = new OnClickListener() { @Override @@ -333,15 +386,25 @@ public class OpenExternalDirectoryActivity extends Activity { mActivity.getExternalStorageClient(), mFile); } if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) { - logValidScopedAccessRequest(activity, directory, + logValidScopedAccessRequest(mActivity, directory, SCOPED_DIRECTORY_ACCESS_DENIED); - activity.setResult(RESULT_CANCELED); + final boolean checked = mDontAskAgain.isChecked(); + if (checked) { + logValidScopedAccessRequest(mActivity, directory, + SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST); + setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(), + mVolumeUuid, directory, PERMISSION_NEVER_ASK); + } else { + setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(), + mVolumeUuid, directory, PERMISSION_ASK_AGAIN); + } + mActivity.setResult(RESULT_CANCELED); } else { - logValidScopedAccessRequest(activity, directory, + logValidScopedAccessRequest(mActivity, directory, SCOPED_DIRECTORY_ACCESS_GRANTED); - activity.setResult(RESULT_OK, intent); + mActivity.setResult(RESULT_OK, intent); } - activity.finish(); + mActivity.finish(); } }; @@ -349,11 +412,31 @@ public class OpenExternalDirectoryActivity extends Activity { .expandTemplate( getText(R.string.open_external_dialog_request), mAppLabel, directory, mVolumeLabel); - return new AlertDialog.Builder(activity, R.style.AlertDialogTheme) - .setMessage(message) + @SuppressLint("InflateParams") + // It's ok pass null ViewRoot on AlertDialogs. + final View view = View.inflate(mActivity, R.layout.dialog_open_scoped_directory, null); + final TextView messageField = (TextView) view.findViewById(R.id.message); + messageField.setText(message); + mDialog = new AlertDialog.Builder(mActivity, R.style.AlertDialogTheme) + .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, directory) == 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 diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto index d36a1d7027010..d929519bddff9 100644 --- a/proto/src/metrics_constants.proto +++ b/proto/src/metrics_constants.proto @@ -1996,6 +1996,22 @@ message MetricsEvent { // Logs that the user docks window via shortcut key. WINDOW_DOCK_SHORTCUTS = 352; + // User already denied access to the request folder; action takes an integer + // representing the folder's index on Environment.STANDARD_DIRECTORIES + ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_FOLDER = 353; + + // User already denied access to the request folder; action pass package name + // of calling package. + ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_PACKAGE = 354; + + // User denied access to the request folder and checked 'Do not ask again'; + // action takes an integer representing the folder's index on Environment.STANDARD_DIRECTORIES + ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_FOLDER = 355; + + // User denied access to the request folder and checked 'Do not ask again'; + // action pass package name of calling package. + ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_PACKAGE = 356; + // Add new aosp constants above this line. // END OF AOSP CONSTANTS }