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 }