Loading src/com/android/documentsui/ScopedAccessActivity.java +42 −164 Original line number Original line Diff line number Diff line Loading @@ -32,6 +32,7 @@ import static com.android.documentsui.ScopedAccessMetrics.logInvalidScopedAccess import static com.android.documentsui.ScopedAccessMetrics.logValidScopedAccessRequest; import static com.android.documentsui.ScopedAccessMetrics.logValidScopedAccessRequest; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT; import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT; import static com.android.documentsui.base.SharedMinimal.getUriPermission; import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName; import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName; import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN; import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN; import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK; import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK; Loading Loading @@ -154,96 +155,20 @@ public class ScopedAccessActivity extends Activity { */ */ private static boolean showFragment(ScopedAccessActivity activity, int userId, private static boolean showFragment(ScopedAccessActivity activity, int userId, StorageVolume storageVolume, String directoryName) { StorageVolume storageVolume, String directoryName) { if (DEBUG) return getUriPermission(activity, Log.d(TAG, "showFragment() for volume " + storageVolume.dump() + ", directory " activity.getExternalStorageClient(), storageVolume, directoryName, userId, true, + directoryName + ", and user " + userId); (file, volumeLabel, isRoot, isPrimary, grantedUri, rootUri) -> { final boolean isRoot = directoryName.equals(DIRECTORY_ROOT); final boolean isPrimary = storageVolume.isPrimary(); if (isRoot && isPrimary) { if (DEBUG) Log.d(TAG, "root access requested on primary volume"); return false; } final File volumeRoot = storageVolume.getPathFile(); File file; try { 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); logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR); return false; } final StorageManager sm = (StorageManager) activity.getSystemService(Context.STORAGE_SERVICE); 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. String volumeLabel = null; String volumeUuid = null; final List<VolumeInfo> volumes = sm.getVolumes(); if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size()); File internalRoot = null; boolean found = true; for (VolumeInfo volume : volumes) { if (isRightVolume(volume, root, userId)) { found = true; internalRoot = volume.getInternalPathForUser(userId); // Must convert path before calling getDocIdForFileCreateNewDir() if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot); file = isRoot ? internalRoot : new File(internalRoot, directory); volumeUuid = storageVolume.getUuid(); volumeLabel = sm.getBestVolumeDescription(volume); if (TextUtils.isEmpty(volumeLabel)) { volumeLabel = storageVolume.getDescription(activity); } if (TextUtils.isEmpty(volumeLabel)) { volumeLabel = activity.getString(android.R.string.unknownName); Log.w(TAG, "No volume description for " + volume + "; using " + volumeLabel); } break; } } if (internalRoot == null) { // Should not happen on normal circumstances, unless app crafted an invalid volume // using reflection or the list of mounted volumes changed. Log.e(TAG, "Didn't find right volume for '" + storageVolume.dump() + "' on " + volumes); return false; } // Checks if the user has granted the permission already. // Checks if the user has granted the permission already. final Intent intent = getIntentForExistingPermission(activity, internalRoot, file); final Intent intent = getIntentForExistingPermission(activity, activity.getCallingPackage(), grantedUri, rootUri); if (intent != null) { if (intent != null) { logValidScopedAccessRequest(activity, directory, logValidScopedAccessRequest(activity, isRoot ? "." : directoryName, SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED); SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED); activity.setResult(RESULT_OK, intent); activity.setResult(RESULT_OK, intent); activity.finish(); activity.finish(); return true; return true; } } if (!found) { Log.e(TAG, "Could not get volume for " + file); logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR); return false; } // Gets the package label. // Gets the package label. final String appLabel = getAppLabel(activity); final String appLabel = getAppLabel(activity); if (appLabel == null) { if (appLabel == null) { Loading @@ -255,7 +180,7 @@ public class ScopedAccessActivity extends Activity { final Bundle args = new Bundle(); final Bundle args = new Bundle(); args.putString(EXTRA_FILE, file.getAbsolutePath()); args.putString(EXTRA_FILE, file.getAbsolutePath()); args.putString(EXTRA_VOLUME_LABEL, volumeLabel); args.putString(EXTRA_VOLUME_LABEL, volumeLabel); args.putString(EXTRA_VOLUME_UUID, volumeUuid); args.putString(EXTRA_VOLUME_UUID, storageVolume.getUuid()); args.putString(EXTRA_APP_LABEL, appLabel); args.putString(EXTRA_APP_LABEL, appLabel); args.putBoolean(EXTRA_IS_ROOT, isRoot); args.putBoolean(EXTRA_IS_ROOT, isRoot); args.putBoolean(EXTRA_IS_PRIMARY, isPrimary); args.putBoolean(EXTRA_IS_PRIMARY, isPrimary); Loading @@ -268,6 +193,8 @@ public class ScopedAccessActivity extends Activity { ft.commitAllowingStateLoss(); ft.commitAllowingStateLoss(); return true; return true; }); } } private static String getAppLabel(Activity activity) { private static String getAppLabel(Activity activity) { Loading @@ -282,52 +209,9 @@ public class ScopedAccessActivity extends Activity { } } } } private static boolean isRightVolume(VolumeInfo volume, String root, int userId) { final File userPath = volume.getPathForUser(userId); final String path = userPath == null ? null : volume.getPathForUser(userId).getPath(); final boolean isMounted = volume.isMountedReadable(); if (DEBUG) Log.d(TAG, "Volume: " + volume + "\n\tuserId: " + userId + "\n\tuserPath: " + userPath + "\n\troot: " + root + "\n\tpath: " + path + "\n\tisMounted: " + isMounted); return isMounted && root.equals(path); } private static Uri getGrantedUriPermission(Context context, ContentProviderClient provider, File file) { // Calls ExternalStorageProvider to get the doc id for the file final Bundle bundle; try { bundle = provider.call("getDocIdForFileCreateNewDir", file.getPath(), null); } catch (RemoteException e) { Log.e(TAG, "Did not get doc id from External Storage provider for " + file, e); logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); return null; } final String docId = bundle == null ? null : bundle.getString("DOC_ID"); if (docId == null) { Log.e(TAG, "Did not get doc id from External Storage provider for " + file); logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); return null; } if (DEBUG) Log.d(TAG, "doc id for " + file + ": " + docId); final Uri uri = DocumentsContract.buildTreeDocumentUri(Providers.AUTHORITY_STORAGE, docId); if (uri == null) { Log.e(TAG, "Could not get URI for doc id " + docId); return null; } if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri); return uri; } private static Intent createGrantedUriPermissionsIntent(Context context, private static Intent createGrantedUriPermissionsIntent(Context context, ContentProviderClient provider, File file) { ContentProviderClient provider, File file) { final Uri uri = getGrantedUriPermission(context, provider, file); final Uri uri = getUriPermission(context, provider, file); return createGrantedUriPermissionsIntent(uri); return createGrantedUriPermissionsIntent(uri); } } Loading @@ -341,19 +225,13 @@ public class ScopedAccessActivity extends Activity { return intent; return intent; } } private static Intent getIntentForExistingPermission(ScopedAccessActivity activity, File root, private static Intent getIntentForExistingPermission(Context context, String packageName, File file) { Uri grantedUri, Uri rootUri) { final String packageName = activity.getCallingPackage(); if (DEBUG) { 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 + ")"); + " or its root (" + rootUri + ")"); final ActivityManager am = } (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE); final ActivityManager am = context.getSystemService(ActivityManager.class); for (GrantedUriPermission uriPermission : am.getGrantedUriPermissions(packageName) for (GrantedUriPermission uriPermission : am.getGrantedUriPermissions(packageName) .getList()) { .getList()) { final Uri uri = uriPermission.uri; final Uri uri = uriPermission.uri; Loading src/com/android/documentsui/base/SharedMinimal.java +188 −1 Original line number Original line Diff line number Diff line Loading @@ -16,10 +16,30 @@ package com.android.documentsui.base; package com.android.documentsui.base; import static android.os.Environment.isStandardDirectory; import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ERROR; import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY; import static com.android.documentsui.ScopedAccessMetrics.logInvalidScopedAccessRequest; import android.annotation.Nullable; import android.annotation.Nullable; import android.content.ContentProviderClient; import android.content.Context; import android.net.Uri; import android.os.Build; import android.os.Build; import android.os.Bundle; import android.os.RemoteException; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.os.storage.VolumeInfo; import android.provider.DocumentsContract; import android.text.TextUtils; import android.util.Log; import android.util.Log; import java.io.File; import java.io.IOException; import java.util.List; /** /** * Contains the minimum number of utilities (contants, helpers, etc...) that can be used by both the * Contains the minimum number of utilities (contants, helpers, etc...) that can be used by both the * main package and the minimal APK that's used by Android TV (and other devices). * main package and the minimal APK that's used by Android TV (and other devices). Loading @@ -34,9 +54,32 @@ public final class SharedMinimal { public static final boolean DEBUG = Build.IS_DEBUGGABLE; public static final boolean DEBUG = Build.IS_DEBUGGABLE; public static final boolean VERBOSE = DEBUG && Log.isLoggable(TAG, Log.VERBOSE); public static final boolean VERBOSE = DEBUG && Log.isLoggable(TAG, Log.VERBOSE); // Special directory name representing the full volume of a scoped directory request. /** * Special directory name representing the full volume of a scoped directory request. */ public static final String DIRECTORY_ROOT = "ROOT_DIRECTORY"; public static final String DIRECTORY_ROOT = "ROOT_DIRECTORY"; /** * Callback for {@link SharedMinimal#getUriPermission(Context, ContentProviderClient, * StorageVolume, String, int, boolean, GetUriPermissionCallback)}. */ public static interface GetUriPermissionCallback { /** * Evaluates the result of the request. * * @param file the path of the requested URI. * @param volumeLabel user-friendly label of the volume. * @param isRoot whether the requested directory is the root directory. * @param isPrimary whether the requested volume is the primary storage volume. * @param requestedUri the requested URI. * @param rootUri the URI for the volume's root directory. * @return whethe the result was sucessfully. */ boolean onResult(File file, String volumeLabel, boolean isRoot, boolean isPrimary, Uri requestedUri, Uri rootUri); } /** /** * Gets the name of a directory name in the format that's used internally by the app * Gets the name of a directory name in the format that's used internally by the app * (i.e., mapping {@code null} to {@link #DIRECTORY_ROOT}); * (i.e., mapping {@code null} to {@link #DIRECTORY_ROOT}); Loading @@ -55,6 +98,150 @@ public final class SharedMinimal { return name.equals(DIRECTORY_ROOT) ? null : name; return name.equals(DIRECTORY_ROOT) ? null : name; } } /** * Gets the URI permission for the given volume and directory. * * @param context caller's context. * @param storageClient storage provider client. * @param storageVolume volume. * @param directoryName directory name, or {@link #DIRECTORY_ROOT} for full volume. * @param userId caller's user handle. * @param logMetrics whether intermediate errors should be logged. * @param callback callback that receives the results. * * @return whether the call was succesfull or not. */ public static boolean getUriPermission(Context context, ContentProviderClient storageClient, StorageVolume storageVolume, String directoryName, int userId, boolean logMetrics, GetUriPermissionCallback callback) { if (DEBUG) { Log.d(TAG, "getUriPermission() for volume " + storageVolume.dump() + ", directory " + directoryName + ", and user " + userId); } final boolean isRoot = directoryName.equals(DIRECTORY_ROOT); final boolean isPrimary = storageVolume.isPrimary(); if (isRoot && isPrimary) { if (DEBUG) Log.d(TAG, "root access requested on primary volume"); return false; } final File volumeRoot = storageVolume.getPathFile(); File file; try { 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); if (logMetrics) logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); return false; } final StorageManager sm = context.getSystemService(StorageManager.class); 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() + "')"); } if (logMetrics) { logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY); } return false; } } // Gets volume label and converted path. String volumeLabel = 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)) { internalRoot = volume.getInternalPathForUser(userId); // Must convert path before calling getDocIdForFileCreateNewDir() if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot); file = isRoot ? internalRoot : new File(internalRoot, directory); volumeLabel = sm.getBestVolumeDescription(volume); if (TextUtils.isEmpty(volumeLabel)) { volumeLabel = storageVolume.getDescription(context); } if (TextUtils.isEmpty(volumeLabel)) { volumeLabel = context.getString(android.R.string.unknownName); Log.w(TAG, "No volume description for " + volume + "; using " + volumeLabel); } break; } } if (internalRoot == null) { // Should not happen on normal circumstances, unless app crafted an invalid volume // using reflection or the list of mounted volumes changed. Log.e(TAG, "Didn't find right volume for '" + storageVolume.dump() + "' on " + volumes); return false; } final Uri requestedUri = getUriPermission(context, storageClient, file); final Uri rootUri = internalRoot.equals(file) ? requestedUri : getUriPermission(context, storageClient, internalRoot); return callback.onResult(file, volumeLabel, isRoot, isPrimary, requestedUri, rootUri); } /** * Creates an URI permission for the given file. */ public static Uri getUriPermission(Context context, ContentProviderClient storageProvider, File file) { // Calls ExternalStorageProvider to get the doc id for the file final Bundle bundle; try { bundle = storageProvider.call("getDocIdForFileCreateNewDir", file.getPath(), null); } catch (RemoteException e) { Log.e(TAG, "Did not get doc id from External Storage provider for " + file, e); logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); return null; } final String docId = bundle == null ? null : bundle.getString("DOC_ID"); if (docId == null) { Log.e(TAG, "Did not get doc id from External Storage provider for " + file); logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); return null; } if (DEBUG) Log.d(TAG, "doc id for " + file + ": " + docId); final Uri uri = DocumentsContract.buildTreeDocumentUri(Providers.AUTHORITY_STORAGE, docId); if (uri == null) { Log.e(TAG, "Could not get URI for doc id " + docId); return null; } if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri); return uri; } private static boolean isRightVolume(VolumeInfo volume, String root, int userId) { final File userPath = volume.getPathForUser(userId); final String path = userPath == null ? null : volume.getPathForUser(userId).getPath(); final boolean isMounted = volume.isMountedReadable(); if (DEBUG) Log.d(TAG, "Volume: " + volume + "\n\tuserId: " + userId + "\n\tuserPath: " + userPath + "\n\troot: " + root + "\n\tpath: " + path + "\n\tisMounted: " + isMounted); return isMounted && root.equals(path); } private SharedMinimal() { private SharedMinimal() { throw new UnsupportedOperationException("provides static fields only"); throw new UnsupportedOperationException("provides static fields only"); } } Loading Loading
src/com/android/documentsui/ScopedAccessActivity.java +42 −164 Original line number Original line Diff line number Diff line Loading @@ -32,6 +32,7 @@ import static com.android.documentsui.ScopedAccessMetrics.logInvalidScopedAccess import static com.android.documentsui.ScopedAccessMetrics.logValidScopedAccessRequest; import static com.android.documentsui.ScopedAccessMetrics.logValidScopedAccessRequest; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT; import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT; import static com.android.documentsui.base.SharedMinimal.getUriPermission; import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName; import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName; import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN; import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN; import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK; import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK; Loading Loading @@ -154,96 +155,20 @@ public class ScopedAccessActivity extends Activity { */ */ private static boolean showFragment(ScopedAccessActivity activity, int userId, private static boolean showFragment(ScopedAccessActivity activity, int userId, StorageVolume storageVolume, String directoryName) { StorageVolume storageVolume, String directoryName) { if (DEBUG) return getUriPermission(activity, Log.d(TAG, "showFragment() for volume " + storageVolume.dump() + ", directory " activity.getExternalStorageClient(), storageVolume, directoryName, userId, true, + directoryName + ", and user " + userId); (file, volumeLabel, isRoot, isPrimary, grantedUri, rootUri) -> { final boolean isRoot = directoryName.equals(DIRECTORY_ROOT); final boolean isPrimary = storageVolume.isPrimary(); if (isRoot && isPrimary) { if (DEBUG) Log.d(TAG, "root access requested on primary volume"); return false; } final File volumeRoot = storageVolume.getPathFile(); File file; try { 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); logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR); return false; } final StorageManager sm = (StorageManager) activity.getSystemService(Context.STORAGE_SERVICE); 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. String volumeLabel = null; String volumeUuid = null; final List<VolumeInfo> volumes = sm.getVolumes(); if (DEBUG) Log.d(TAG, "Number of volumes: " + volumes.size()); File internalRoot = null; boolean found = true; for (VolumeInfo volume : volumes) { if (isRightVolume(volume, root, userId)) { found = true; internalRoot = volume.getInternalPathForUser(userId); // Must convert path before calling getDocIdForFileCreateNewDir() if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot); file = isRoot ? internalRoot : new File(internalRoot, directory); volumeUuid = storageVolume.getUuid(); volumeLabel = sm.getBestVolumeDescription(volume); if (TextUtils.isEmpty(volumeLabel)) { volumeLabel = storageVolume.getDescription(activity); } if (TextUtils.isEmpty(volumeLabel)) { volumeLabel = activity.getString(android.R.string.unknownName); Log.w(TAG, "No volume description for " + volume + "; using " + volumeLabel); } break; } } if (internalRoot == null) { // Should not happen on normal circumstances, unless app crafted an invalid volume // using reflection or the list of mounted volumes changed. Log.e(TAG, "Didn't find right volume for '" + storageVolume.dump() + "' on " + volumes); return false; } // Checks if the user has granted the permission already. // Checks if the user has granted the permission already. final Intent intent = getIntentForExistingPermission(activity, internalRoot, file); final Intent intent = getIntentForExistingPermission(activity, activity.getCallingPackage(), grantedUri, rootUri); if (intent != null) { if (intent != null) { logValidScopedAccessRequest(activity, directory, logValidScopedAccessRequest(activity, isRoot ? "." : directoryName, SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED); SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED); activity.setResult(RESULT_OK, intent); activity.setResult(RESULT_OK, intent); activity.finish(); activity.finish(); return true; return true; } } if (!found) { Log.e(TAG, "Could not get volume for " + file); logInvalidScopedAccessRequest(activity, SCOPED_DIRECTORY_ACCESS_ERROR); return false; } // Gets the package label. // Gets the package label. final String appLabel = getAppLabel(activity); final String appLabel = getAppLabel(activity); if (appLabel == null) { if (appLabel == null) { Loading @@ -255,7 +180,7 @@ public class ScopedAccessActivity extends Activity { final Bundle args = new Bundle(); final Bundle args = new Bundle(); args.putString(EXTRA_FILE, file.getAbsolutePath()); args.putString(EXTRA_FILE, file.getAbsolutePath()); args.putString(EXTRA_VOLUME_LABEL, volumeLabel); args.putString(EXTRA_VOLUME_LABEL, volumeLabel); args.putString(EXTRA_VOLUME_UUID, volumeUuid); args.putString(EXTRA_VOLUME_UUID, storageVolume.getUuid()); args.putString(EXTRA_APP_LABEL, appLabel); args.putString(EXTRA_APP_LABEL, appLabel); args.putBoolean(EXTRA_IS_ROOT, isRoot); args.putBoolean(EXTRA_IS_ROOT, isRoot); args.putBoolean(EXTRA_IS_PRIMARY, isPrimary); args.putBoolean(EXTRA_IS_PRIMARY, isPrimary); Loading @@ -268,6 +193,8 @@ public class ScopedAccessActivity extends Activity { ft.commitAllowingStateLoss(); ft.commitAllowingStateLoss(); return true; return true; }); } } private static String getAppLabel(Activity activity) { private static String getAppLabel(Activity activity) { Loading @@ -282,52 +209,9 @@ public class ScopedAccessActivity extends Activity { } } } } private static boolean isRightVolume(VolumeInfo volume, String root, int userId) { final File userPath = volume.getPathForUser(userId); final String path = userPath == null ? null : volume.getPathForUser(userId).getPath(); final boolean isMounted = volume.isMountedReadable(); if (DEBUG) Log.d(TAG, "Volume: " + volume + "\n\tuserId: " + userId + "\n\tuserPath: " + userPath + "\n\troot: " + root + "\n\tpath: " + path + "\n\tisMounted: " + isMounted); return isMounted && root.equals(path); } private static Uri getGrantedUriPermission(Context context, ContentProviderClient provider, File file) { // Calls ExternalStorageProvider to get the doc id for the file final Bundle bundle; try { bundle = provider.call("getDocIdForFileCreateNewDir", file.getPath(), null); } catch (RemoteException e) { Log.e(TAG, "Did not get doc id from External Storage provider for " + file, e); logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); return null; } final String docId = bundle == null ? null : bundle.getString("DOC_ID"); if (docId == null) { Log.e(TAG, "Did not get doc id from External Storage provider for " + file); logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); return null; } if (DEBUG) Log.d(TAG, "doc id for " + file + ": " + docId); final Uri uri = DocumentsContract.buildTreeDocumentUri(Providers.AUTHORITY_STORAGE, docId); if (uri == null) { Log.e(TAG, "Could not get URI for doc id " + docId); return null; } if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri); return uri; } private static Intent createGrantedUriPermissionsIntent(Context context, private static Intent createGrantedUriPermissionsIntent(Context context, ContentProviderClient provider, File file) { ContentProviderClient provider, File file) { final Uri uri = getGrantedUriPermission(context, provider, file); final Uri uri = getUriPermission(context, provider, file); return createGrantedUriPermissionsIntent(uri); return createGrantedUriPermissionsIntent(uri); } } Loading @@ -341,19 +225,13 @@ public class ScopedAccessActivity extends Activity { return intent; return intent; } } private static Intent getIntentForExistingPermission(ScopedAccessActivity activity, File root, private static Intent getIntentForExistingPermission(Context context, String packageName, File file) { Uri grantedUri, Uri rootUri) { final String packageName = activity.getCallingPackage(); if (DEBUG) { 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 + ")"); + " or its root (" + rootUri + ")"); final ActivityManager am = } (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE); final ActivityManager am = context.getSystemService(ActivityManager.class); for (GrantedUriPermission uriPermission : am.getGrantedUriPermissions(packageName) for (GrantedUriPermission uriPermission : am.getGrantedUriPermissions(packageName) .getList()) { .getList()) { final Uri uri = uriPermission.uri; final Uri uri = uriPermission.uri; Loading
src/com/android/documentsui/base/SharedMinimal.java +188 −1 Original line number Original line Diff line number Diff line Loading @@ -16,10 +16,30 @@ package com.android.documentsui.base; package com.android.documentsui.base; import static android.os.Environment.isStandardDirectory; import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ERROR; import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY; import static com.android.documentsui.ScopedAccessMetrics.logInvalidScopedAccessRequest; import android.annotation.Nullable; import android.annotation.Nullable; import android.content.ContentProviderClient; import android.content.Context; import android.net.Uri; import android.os.Build; import android.os.Build; import android.os.Bundle; import android.os.RemoteException; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.os.storage.VolumeInfo; import android.provider.DocumentsContract; import android.text.TextUtils; import android.util.Log; import android.util.Log; import java.io.File; import java.io.IOException; import java.util.List; /** /** * Contains the minimum number of utilities (contants, helpers, etc...) that can be used by both the * Contains the minimum number of utilities (contants, helpers, etc...) that can be used by both the * main package and the minimal APK that's used by Android TV (and other devices). * main package and the minimal APK that's used by Android TV (and other devices). Loading @@ -34,9 +54,32 @@ public final class SharedMinimal { public static final boolean DEBUG = Build.IS_DEBUGGABLE; public static final boolean DEBUG = Build.IS_DEBUGGABLE; public static final boolean VERBOSE = DEBUG && Log.isLoggable(TAG, Log.VERBOSE); public static final boolean VERBOSE = DEBUG && Log.isLoggable(TAG, Log.VERBOSE); // Special directory name representing the full volume of a scoped directory request. /** * Special directory name representing the full volume of a scoped directory request. */ public static final String DIRECTORY_ROOT = "ROOT_DIRECTORY"; public static final String DIRECTORY_ROOT = "ROOT_DIRECTORY"; /** * Callback for {@link SharedMinimal#getUriPermission(Context, ContentProviderClient, * StorageVolume, String, int, boolean, GetUriPermissionCallback)}. */ public static interface GetUriPermissionCallback { /** * Evaluates the result of the request. * * @param file the path of the requested URI. * @param volumeLabel user-friendly label of the volume. * @param isRoot whether the requested directory is the root directory. * @param isPrimary whether the requested volume is the primary storage volume. * @param requestedUri the requested URI. * @param rootUri the URI for the volume's root directory. * @return whethe the result was sucessfully. */ boolean onResult(File file, String volumeLabel, boolean isRoot, boolean isPrimary, Uri requestedUri, Uri rootUri); } /** /** * Gets the name of a directory name in the format that's used internally by the app * Gets the name of a directory name in the format that's used internally by the app * (i.e., mapping {@code null} to {@link #DIRECTORY_ROOT}); * (i.e., mapping {@code null} to {@link #DIRECTORY_ROOT}); Loading @@ -55,6 +98,150 @@ public final class SharedMinimal { return name.equals(DIRECTORY_ROOT) ? null : name; return name.equals(DIRECTORY_ROOT) ? null : name; } } /** * Gets the URI permission for the given volume and directory. * * @param context caller's context. * @param storageClient storage provider client. * @param storageVolume volume. * @param directoryName directory name, or {@link #DIRECTORY_ROOT} for full volume. * @param userId caller's user handle. * @param logMetrics whether intermediate errors should be logged. * @param callback callback that receives the results. * * @return whether the call was succesfull or not. */ public static boolean getUriPermission(Context context, ContentProviderClient storageClient, StorageVolume storageVolume, String directoryName, int userId, boolean logMetrics, GetUriPermissionCallback callback) { if (DEBUG) { Log.d(TAG, "getUriPermission() for volume " + storageVolume.dump() + ", directory " + directoryName + ", and user " + userId); } final boolean isRoot = directoryName.equals(DIRECTORY_ROOT); final boolean isPrimary = storageVolume.isPrimary(); if (isRoot && isPrimary) { if (DEBUG) Log.d(TAG, "root access requested on primary volume"); return false; } final File volumeRoot = storageVolume.getPathFile(); File file; try { 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); if (logMetrics) logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); return false; } final StorageManager sm = context.getSystemService(StorageManager.class); 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() + "')"); } if (logMetrics) { logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY); } return false; } } // Gets volume label and converted path. String volumeLabel = 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)) { internalRoot = volume.getInternalPathForUser(userId); // Must convert path before calling getDocIdForFileCreateNewDir() if (DEBUG) Log.d(TAG, "Converting " + root + " to " + internalRoot); file = isRoot ? internalRoot : new File(internalRoot, directory); volumeLabel = sm.getBestVolumeDescription(volume); if (TextUtils.isEmpty(volumeLabel)) { volumeLabel = storageVolume.getDescription(context); } if (TextUtils.isEmpty(volumeLabel)) { volumeLabel = context.getString(android.R.string.unknownName); Log.w(TAG, "No volume description for " + volume + "; using " + volumeLabel); } break; } } if (internalRoot == null) { // Should not happen on normal circumstances, unless app crafted an invalid volume // using reflection or the list of mounted volumes changed. Log.e(TAG, "Didn't find right volume for '" + storageVolume.dump() + "' on " + volumes); return false; } final Uri requestedUri = getUriPermission(context, storageClient, file); final Uri rootUri = internalRoot.equals(file) ? requestedUri : getUriPermission(context, storageClient, internalRoot); return callback.onResult(file, volumeLabel, isRoot, isPrimary, requestedUri, rootUri); } /** * Creates an URI permission for the given file. */ public static Uri getUriPermission(Context context, ContentProviderClient storageProvider, File file) { // Calls ExternalStorageProvider to get the doc id for the file final Bundle bundle; try { bundle = storageProvider.call("getDocIdForFileCreateNewDir", file.getPath(), null); } catch (RemoteException e) { Log.e(TAG, "Did not get doc id from External Storage provider for " + file, e); logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); return null; } final String docId = bundle == null ? null : bundle.getString("DOC_ID"); if (docId == null) { Log.e(TAG, "Did not get doc id from External Storage provider for " + file); logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR); return null; } if (DEBUG) Log.d(TAG, "doc id for " + file + ": " + docId); final Uri uri = DocumentsContract.buildTreeDocumentUri(Providers.AUTHORITY_STORAGE, docId); if (uri == null) { Log.e(TAG, "Could not get URI for doc id " + docId); return null; } if (DEBUG) Log.d(TAG, "URI for " + file + ": " + uri); return uri; } private static boolean isRightVolume(VolumeInfo volume, String root, int userId) { final File userPath = volume.getPathForUser(userId); final String path = userPath == null ? null : volume.getPathForUser(userId).getPath(); final boolean isMounted = volume.isMountedReadable(); if (DEBUG) Log.d(TAG, "Volume: " + volume + "\n\tuserId: " + userId + "\n\tuserPath: " + userPath + "\n\troot: " + root + "\n\tpath: " + path + "\n\tisMounted: " + isMounted); return isMounted && root.equals(path); } private SharedMinimal() { private SharedMinimal() { throw new UnsupportedOperationException("provides static fields only"); throw new UnsupportedOperationException("provides static fields only"); } } Loading