Merge "Allow Scoped Directory Access on whole volume." into nyc-dev
am: 9eb5555
* commit '9eb5555aa6788ec948e7af8666a9155792b684f8':
Allow Scoped Directory Access on whole volume.
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user