summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Felipe Leme <felipeal@google.com> 2016-03-17 18:56:20 -0700
committer Felipe Leme <felipeal@google.com> 2016-03-21 14:25:17 -0700
commitdb892b84e513f174d29f67d293435407a0ac7c6c (patch)
tree35ba7b3dba1f997054c03729edbefc15bf7b2ad6
parent1981e602ad97e3a21bc987dbeb0625e87a58ff8d (diff)
Allow Scoped Directory Access on whole volume.
There are some scenarios where an app needs access to the whole SD Card, not subdirectories. For example, user might have a SDCard with directories like vacation_pictures (instead of Pictures/vacation); another example is a file management app. BUG: 27676858 Change-Id: I20ef713de7e4dfa7e2d7d07bab11898af186d673
-rw-r--r--core/java/android/os/storage/StorageVolume.java11
-rw-r--r--packages/DocumentsUI/res/values/strings.xml3
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java3
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/Metrics.java12
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java91
-rw-r--r--proto/src/metrics_constants.proto5
6 files changed, 81 insertions, 44 deletions
diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java
index 54d20d361a26..17176ecf1c7c 100644
--- a/core/java/android/os/storage/StorageVolume.java
+++ b/core/java/android/os/storage/StorageVolume.java
@@ -306,8 +306,8 @@ public final class StorageVolume implements Parcelable {
}
/**
- * Builds an intent to give access to a standard storage directory after obtaining the user's
- * approval.
+ * Builds an intent to give access to a standard storage directory or entire volume after
+ * obtaining the user's approval.
* <p>
* When invoked, the system will ask the user to grant access to the requested directory (and
* its descendants). The result of the request will be returned to the activity through the
@@ -322,12 +322,17 @@ public final class StorageVolume implements Parcelable {
* {@link Context#getExternalCacheDirs()}, or
* {@link Context#getExternalMediaDirs()}, which require no permissions to read or write.
*
+ * <strong>NOTE: </strong>requesting access to the entire volume is not recommended and it will
+ * result in a stronger message displayed to the user, which may cause the user to reject
+ * the request.
+ *
* @param directoryName must be one of
* {@link Environment#DIRECTORY_MUSIC}, {@link Environment#DIRECTORY_PODCASTS},
* {@link Environment#DIRECTORY_RINGTONES}, {@link Environment#DIRECTORY_ALARMS},
* {@link Environment#DIRECTORY_NOTIFICATIONS}, {@link Environment#DIRECTORY_PICTURES},
* {@link Environment#DIRECTORY_MOVIES}, {@link Environment#DIRECTORY_DOWNLOADS},
- * {@link Environment#DIRECTORY_DCIM}, or {@link Environment#DIRECTORY_DOCUMENTS}
+ * {@link Environment#DIRECTORY_DCIM}, or {@link Environment#DIRECTORY_DOCUMENTS}, or
+ * {code null} to request access to the entire volume.
*
* @see DocumentsContract
*/
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 0d098e69b73b..b26ee9778d33 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -204,6 +204,9 @@
<string name="open_external_dialog_request">Grant <xliff:g id="appName" example="System Settings"><b>^1</b></xliff:g>
access to <xliff:g id="directory" example="Pictures"><i>^2</i></xliff:g> directory on
<xliff:g id="storage" example="SD Card"><i>^3</i></xliff:g>?</string>
+ <!-- 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. -->
diff --git a/packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java b/packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java
index 8c4859f52a02..2315664f0d8a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/LocalPreferences.java
@@ -16,8 +16,6 @@
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;
@@ -29,7 +27,6 @@ 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;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Metrics.java b/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
index deef1c278e63..929d1e0d0ac6 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
@@ -427,10 +427,14 @@ public final class Metrics {
public static void logValidScopedAccessRequest(Activity activity, String directory,
@ScopedAccessGrant int type) {
int index = -1;
- for (int i = 0; i < STANDARD_DIRECTORIES.length; i++) {
- if (STANDARD_DIRECTORIES[i].equals(directory)) {
- index = i;
- break;
+ if (OpenExternalDirectoryActivity.DIRECTORY_ROOT.equals(directory)) {
+ index = -2;
+ } else {
+ for (int i = 0; i < STANDARD_DIRECTORIES.length; i++) {
+ if (STANDARD_DIRECTORIES[i].equals(directory)) {
+ index = i;
+ break;
+ }
}
}
final String packageName = activity.getCallingPackage();
diff --git a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
index 2b6f3968e203..dd7eaf99ad84 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
@@ -85,6 +85,9 @@ public class OpenExternalDirectoryActivity extends Activity {
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";
+ // Special directory name representing the full volume
+ static final String DIRECTORY_ROOT = "ROOT_DIRECTORY";
private ContentProviderClient mExternalStorageClient;
@@ -114,13 +117,9 @@ public class OpenExternalDirectoryActivity extends Activity {
finish();
return;
}
- final String directoryName = intent.getStringExtra(EXTRA_DIRECTORY_NAME);
+ String directoryName = intent.getStringExtra(EXTRA_DIRECTORY_NAME );
if (directoryName == null) {
- logInvalidScopedAccessRequest(this, SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS);
- if (DEBUG) Log.d(TAG, "missing extra " + EXTRA_DIRECTORY_NAME + " on " + intent);
- setResult(RESULT_CANCELED);
- finish();
- return;
+ directoryName = DIRECTORY_ROOT;
}
final StorageVolume volume = (StorageVolume) storageVolume;
if (getScopedAccessPermissionStatus(getApplicationContext(), getCallingPackage(),
@@ -157,9 +156,11 @@ public class OpenExternalDirectoryActivity extends Activity {
if (DEBUG)
Log.d(TAG, "showFragment() for volume " + storageVolume.dump() + ", directory "
+ directoryName + ", and user " + userId);
+ final boolean isRoot = directoryName.equals(DIRECTORY_ROOT);
+ final File volumeRoot = storageVolume.getPathFile();
File file;
try {
- file = new File(storageVolume.getPathFile(), directoryName).getCanonicalFile();
+ file = isRoot ? volumeRoot : new File(volumeRoot, directoryName).getCanonicalFile();
} catch (IOException e) {
Log.e(TAG, "Could not get canonical file for volume " + storageVolume.dump()
+ " and directory " + directoryName);
@@ -169,16 +170,21 @@ public class OpenExternalDirectoryActivity extends Activity {
final StorageManager sm =
(StorageManager) activity.getSystemService(Context.STORAGE_SERVICE);
- final String root = file.getParent();
- final String directory = file.getName();
-
- // Verify directory is valid.
- if (TextUtils.isEmpty(directory) || !isStandardDirectory(directory)) {
- if (DEBUG)
- Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '"
- + file.getAbsolutePath() + "')");
- logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY);
- return false;
+ final String root, directory;
+ if (isRoot) {
+ root = volumeRoot.getAbsolutePath();
+ directory = ".";
+ } else {
+ root = file.getParent();
+ directory = file.getName();
+ // Verify directory is valid.
+ if (TextUtils.isEmpty(directory) || !isStandardDirectory(directory)) {
+ if (DEBUG)
+ Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '"
+ + file.getAbsolutePath() + "')");
+ logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY);
+ return false;
+ }
}
// Gets volume label and converted path.
@@ -186,12 +192,13 @@ public class OpenExternalDirectoryActivity extends Activity {
String volumeUuid = null;
final List<VolumeInfo> volumes = sm.getVolumes();
if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size());
+ File internalRoot = null;
for (VolumeInfo volume : volumes) {
if (isRightVolume(volume, root, userId)) {
- final File internalRoot = volume.getInternalPathForUser(userId);
+ internalRoot = volume.getInternalPathForUser(userId);
// Must convert path before calling getDocIdForFileCreateNewDir()
if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot);
- file = new File(internalRoot, directory);
+ file = isRoot ? internalRoot : new File(internalRoot, directory);
volumeLabel = sm.getBestVolumeDescription(volume);
volumeUuid = volume.getFsUuid();
break;
@@ -199,7 +206,7 @@ public class OpenExternalDirectoryActivity extends Activity {
}
// Checks if the user has granted the permission already.
- final Intent intent = getIntentForExistingPermission(activity, file);
+ final Intent intent = getIntentForExistingPermission(activity, isRoot, internalRoot, file);
if (intent != null) {
logValidScopedAccessRequest(activity, directory,
SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED);
@@ -227,6 +234,7 @@ public class OpenExternalDirectoryActivity extends Activity {
args.putString(EXTRA_VOLUME_LABEL, volumeLabel);
args.putString(EXTRA_VOLUME_UUID, volumeUuid);
args.putString(EXTRA_APP_LABEL, appLabel);
+ args.putBoolean(EXTRA_IS_ROOT, isRoot);
final FragmentManager fm = activity.getFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
@@ -310,19 +318,27 @@ public class OpenExternalDirectoryActivity extends Activity {
}
private static Intent getIntentForExistingPermission(OpenExternalDirectoryActivity activity,
- File file) {
+ boolean isRoot, File root, File file) {
final String packageName = activity.getCallingPackage();
- final Uri grantedUri =
- getGrantedUriPermission(activity, activity.getExternalStorageClient(), file);
+ final ContentProviderClient storageClient = activity.getExternalStorageClient();
+ final Uri grantedUri = getGrantedUriPermission(activity, storageClient, file);
+ final Uri rootUri = root.equals(file) ? grantedUri
+ : getGrantedUriPermission(activity, storageClient, root);
+
if (DEBUG)
- Log.d(TAG, "checking if " + packageName + " already has permission for " + grantedUri);
+ Log.d(TAG, "checking if " + packageName + " already has permission for " + grantedUri
+ + " or its root (" + rootUri + ")");
final ActivityManager am =
(ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
for (UriPermission uriPermission : am.getGrantedUriPermissions(packageName).getList()) {
final Uri uri = uriPermission.getUri();
- if (uri.equals(grantedUri)) {
+ 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(uri);
+ return createGrantedUriPermissionsIntent(grantedUri);
}
}
if (DEBUG) Log.d(TAG, packageName + " does not have permission for " + grantedUri);
@@ -335,6 +351,7 @@ public class OpenExternalDirectoryActivity extends Activity {
private String mVolumeUuid;
private String mVolumeLabel;
private String mAppLabel;
+ private boolean mIsRoot;
private CheckBox mDontAskAgain;
private OpenExternalDirectoryActivity mActivity;
private AlertDialog mDialog;
@@ -349,6 +366,7 @@ public class OpenExternalDirectoryActivity extends Activity {
mVolumeUuid = args.getString(EXTRA_VOLUME_UUID);
mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL);
mAppLabel = args.getString(EXTRA_APP_LABEL);
+ mIsRoot = args.getBoolean(EXTRA_IS_ROOT);
}
mActivity = (OpenExternalDirectoryActivity) getActivity();
}
@@ -375,6 +393,7 @@ public class OpenExternalDirectoryActivity extends Activity {
mActivity = (OpenExternalDirectoryActivity) getActivity();
}
final String directory = mFile.getName();
+ final String directoryName = mIsRoot ? DIRECTORY_ROOT : directory;
final Context context = mActivity.getApplicationContext();
final OnClickListener listener = new OnClickListener() {
@@ -386,17 +405,17 @@ public class OpenExternalDirectoryActivity extends Activity {
mActivity.getExternalStorageClient(), mFile);
}
if (which == DialogInterface.BUTTON_NEGATIVE || intent == null) {
- logValidScopedAccessRequest(mActivity, directory,
+ 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, directory, PERMISSION_NEVER_ASK);
+ mVolumeUuid, directoryName, PERMISSION_NEVER_ASK);
} else {
setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
- mVolumeUuid, directory, PERMISSION_ASK_AGAIN);
+ mVolumeUuid, directoryName, PERMISSION_ASK_AGAIN);
}
mActivity.setResult(RESULT_CANCELED);
} else {
@@ -408,13 +427,17 @@ public class OpenExternalDirectoryActivity extends Activity {
}
};
- final CharSequence message = TextUtils
- .expandTemplate(
- getText(R.string.open_external_dialog_request), mAppLabel, directory,
- mVolumeLabel);
@SuppressLint("InflateParams")
// 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(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)
@@ -425,7 +448,7 @@ public class OpenExternalDirectoryActivity extends Activity {
mDontAskAgain = (CheckBox) view.findViewById(R.id.do_not_ask_checkbox);
if (getScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
- mVolumeUuid, directory) == PERMISSION_ASK_AGAIN) {
+ mVolumeUuid, directoryName) == PERMISSION_ASK_AGAIN) {
mDontAskAgain.setVisibility(View.VISIBLE);
mDontAskAgain.setOnCheckedChangeListener(new OnCheckedChangeListener() {
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index d929519bddff..d6f1499ef684 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -1922,10 +1922,12 @@ message MetricsEvent {
// User granted access to the request folder; action takes an integer
// representing the folder's index on Environment.STANDARD_DIRECTORIES
+ // (or -2 for root access, or -1 or unknown directory).
ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_FOLDER = 326;
// User denied access to the request folder; action takes an integer
// representing the folder's index on Environment.STANDARD_DIRECTORIES
+ // (or -2 for root access, or -1 or unknown directory).
ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_FOLDER = 327;
// User granted access to the request folder; action pass package name
@@ -1939,6 +1941,7 @@ message MetricsEvent {
// App requested access to a directory it has already been granted
// access before; action takes an integer representing the folder's
// index on Environment.STANDARD_DIRECTORIES
+ // (or -2 for root access, or -1 or unknown directory).
ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_FOLDER = 330;
// App requested access to a directory it has already been granted
@@ -1998,6 +2001,7 @@ message MetricsEvent {
// User already denied access to the request folder; action takes an integer
// representing the folder's index on Environment.STANDARD_DIRECTORIES
+ // (or -2 for root access, or -1 or unknown directory).
ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_FOLDER = 353;
// User already denied access to the request folder; action pass package name
@@ -2006,6 +2010,7 @@ message MetricsEvent {
// 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
+ // (or -2 for root access, or -1 or unknown directory).
ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_FOLDER = 355;
// User denied access to the request folder and checked 'Do not ask again';