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:
+ *
+ * - {@code USER_ID|PACKAGE_NAME|VOLUME_UUID|DIRECTORY} for storage volumes that have a UUID
+ * (typically physical volumes like SD cards).
+ *
- {@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
}