API 30: prep to request access by directory, not volume

This commit is contained in:
Thibault Deckers 2020-07-19 22:00:59 +09:00
parent ffc989d9a3
commit 51fb36bb70
5 changed files with 37 additions and 67 deletions

View file

@ -100,7 +100,7 @@ public class MainActivity extends FlutterActivity {
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == PermissionManager.VOLUME_ROOT_PERMISSION_REQUEST_CODE) {
if (resultCode != RESULT_OK || data.getData() == null) {
PermissionManager.onPermissionResult(this, requestCode, false, null);
PermissionManager.onPermissionResult(this, requestCode, null);
return;
}
@ -113,7 +113,7 @@ public class MainActivity extends FlutterActivity {
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
// resume pending action
PermissionManager.onPermissionResult(this, requestCode, true, treeUri);
PermissionManager.onPermissionResult(this, requestCode, treeUri);
}
}
}

View file

@ -17,14 +17,14 @@ public class StorageAccessStreamHandler implements EventChannel.StreamHandler {
private Activity activity;
private EventChannel.EventSink eventSink;
private Handler handler;
private String volumePath;
private String path;
@SuppressWarnings("unchecked")
public StorageAccessStreamHandler(Activity activity, Object arguments) {
this.activity = activity;
if (arguments instanceof Map) {
Map<String, Object> argMap = (Map<String, Object>) arguments;
this.volumePath = (String) argMap.get("path");
this.path = (String) argMap.get("path");
}
}
@ -32,9 +32,9 @@ public class StorageAccessStreamHandler implements EventChannel.StreamHandler {
public void onListen(Object o, final EventChannel.EventSink eventSink) {
this.eventSink = eventSink;
this.handler = new Handler(Looper.getMainLooper());
Runnable onGranted = () -> success(!PermissionManager.requireVolumeAccessDialog(activity, volumePath));
Runnable onGranted = () -> success(!PermissionManager.requireVolumeAccessDialog(activity, path));
Runnable onDenied = () -> success(false);
PermissionManager.requestVolumeAccess(activity, volumePath, onGranted, onDenied);
PermissionManager.requestVolumeAccess(activity, path, onGranted, onDenied);
}
@Override

View file

@ -9,8 +9,6 @@ import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.util.Log;
@ -32,7 +30,6 @@ import java.util.Map;
import deckers.thibault.aves.model.AvesImageEntry;
import deckers.thibault.aves.utils.MetadataHelper;
import deckers.thibault.aves.utils.MimeTypes;
import deckers.thibault.aves.utils.PermissionManager;
import deckers.thibault.aves.utils.StorageUtils;
import deckers.thibault.aves.utils.Utils;
@ -74,12 +71,6 @@ public abstract class ImageProvider {
return;
}
if (PermissionManager.requireVolumeAccessDialog(activity, oldPath)) {
Runnable runnable = () -> rename(activity, oldPath, oldMediaUri, mimeType, newFilename, callback);
new Handler(Looper.getMainLooper()).post(() -> PermissionManager.showVolumeAccessDialog(activity, oldPath, runnable));
return;
}
DocumentFileCompat df = StorageUtils.getDocumentFile(activity, oldPath, oldMediaUri);
try {
boolean renamed = df != null && df.renameTo(newFilename);
@ -97,12 +88,6 @@ public abstract class ImageProvider {
}
public void rotate(final Activity activity, final String path, final Uri uri, final String mimeType, final boolean clockwise, final ImageOpCallback callback) {
if (PermissionManager.requireVolumeAccessDialog(activity, path)) {
Runnable runnable = () -> rotate(activity, path, uri, mimeType, clockwise, callback);
new Handler(Looper.getMainLooper()).post(() -> PermissionManager.showVolumeAccessDialog(activity, path, runnable));
return;
}
switch (mimeType) {
case MimeTypes.JPEG:
rotateJpeg(activity, path, uri, clockwise, callback);

View file

@ -8,8 +8,6 @@ import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.MediaStore;
@ -35,7 +33,6 @@ import java.util.stream.Stream;
import deckers.thibault.aves.model.AvesImageEntry;
import deckers.thibault.aves.model.SourceImageEntry;
import deckers.thibault.aves.utils.MimeTypes;
import deckers.thibault.aves.utils.PermissionManager;
import deckers.thibault.aves.utils.StorageUtils;
import deckers.thibault.aves.utils.Utils;
@ -217,18 +214,6 @@ public class MediaStoreImageProvider extends ImageProvider {
SettableFuture<Object> future = SettableFuture.create();
if (StorageUtils.requireAccessPermission(path)) {
if (PermissionManager.getVolumeTreeUri(activity, path) == null) {
Runnable runnable = () -> {
try {
future.set(delete(activity, path, mediaUri).get());
} catch (Exception e) {
future.setException(e);
}
};
new Handler(Looper.getMainLooper()).post(() -> PermissionManager.showVolumeAccessDialog(activity, path, runnable));
return future;
}
// if the file is on SD card, calling the content resolver delete() removes the entry from the Media Store
// but it doesn't delete the file, even if the app has the permission
try {
@ -276,12 +261,6 @@ public class MediaStoreImageProvider extends ImageProvider {
@Override
public void moveMultiple(final Activity activity, final Boolean copy, final String destinationDir, final List<AvesImageEntry> entries, @NonNull final ImageOpCallback callback) {
if (PermissionManager.requireVolumeAccessDialog(activity, destinationDir)) {
Runnable runnable = () -> moveMultiple(activity, copy, destinationDir, entries, callback);
new Handler(Looper.getMainLooper()).post(() -> PermissionManager.showVolumeAccessDialog(activity, destinationDir, runnable));
return;
}
DocumentFileCompat destinationDirDocFile = StorageUtils.createDirectoryIfAbsent(activity, destinationDir);
if (destinationDirDocFile == null) {
callback.onFailure(new Exception("failed to create directory at path=" + destinationDir));

View file

@ -41,26 +41,15 @@ public class PermissionManager {
return uriPermissionOptional.map(UriPermission::getUri).orElse(null);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public static void showVolumeAccessDialog(final Activity activity, @NonNull String anyPath, final Runnable pendingRunnable) {
String volumePath = StorageUtils.getVolumePath(activity, anyPath).orElse(null);
// TODO TLAD show volume name/ID in the message
new AlertDialog.Builder(activity)
.setTitle("Storage Volume Access")
.setMessage("Please select the root directory of the storage volume in the next screen, so that this app has permission to access it and complete your request.")
.setPositiveButton(android.R.string.ok, (dialog, button) -> requestVolumeAccess(activity, volumePath, pendingRunnable, null))
.show();
}
public static void requestVolumeAccess(Activity activity, String volumePath, Runnable onGranted, Runnable onDenied) {
Log.i(LOG_TAG, "request user to select and grant access permission to volume=" + volumePath);
pendingPermissionMap.put(VOLUME_ROOT_PERMISSION_REQUEST_CODE, new PendingPermissionHandler(volumePath, onGranted, onDenied));
public static void requestVolumeAccess(@NonNull Activity activity, @NonNull String path, @NonNull Runnable onGranted, @NonNull Runnable onDenied) {
Log.i(LOG_TAG, "request user to select and grant access permission to volume=" + path);
pendingPermissionMap.put(VOLUME_ROOT_PERMISSION_REQUEST_CODE, new PendingPermissionHandler(path, onGranted, onDenied));
Intent intent = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && volumePath != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
StorageManager sm = activity.getSystemService(StorageManager.class);
if (sm != null) {
StorageVolume volume = sm.getStorageVolume(new File(volumePath));
StorageVolume volume = sm.getStorageVolume(new File(path));
if (volume != null) {
intent = volume.createOpenDocumentTreeIntent();
}
@ -75,25 +64,42 @@ public class PermissionManager {
ActivityCompat.startActivityForResult(activity, intent, VOLUME_ROOT_PERMISSION_REQUEST_CODE, null);
}
public static void onPermissionResult(Activity activity, int requestCode, boolean granted, Uri treeUri) {
Log.d(LOG_TAG, "onPermissionResult with requestCode=" + requestCode + ", granted=" + granted);
public static void onPermissionResult(Activity activity, int requestCode, @Nullable Uri treeUri) {
Log.d(LOG_TAG, "onPermissionResult with requestCode=" + requestCode + ", treeUri=" + treeUri);
boolean granted = treeUri != null;
PendingPermissionHandler handler = pendingPermissionMap.remove(requestCode);
if (handler == null) return;
StorageUtils.setVolumeTreeUri(activity, handler.volumePath, treeUri.toString());
if (granted) {
String requestedPath = handler.path;
if (isTreeUriPath(requestedPath, treeUri)) {
StorageUtils.setVolumeTreeUri(activity, requestedPath, treeUri.toString());
} else {
granted = false;
}
}
Runnable runnable = granted ? handler.onGranted : handler.onDenied;
if (runnable == null) return;
runnable.run();
}
static class PendingPermissionHandler {
String volumePath;
Runnable onGranted;
Runnable onDenied;
private static boolean isTreeUriPath(String path, Uri treeUri) {
// TODO TLAD check requestedPath match treeUri
// e.g. OK match for path=/storage/emulated/0/, treeUri=content://com.android.externalstorage.documents/tree/primary%3A
// e.g. NO match for path=/storage/10F9-3F13/, treeUri=content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures
Log.d(LOG_TAG, "isTreeUriPath path=" + path + ", treeUri=" + treeUri);
return true;
}
PendingPermissionHandler(String volumePath, Runnable onGranted, Runnable onDenied) {
this.volumePath = volumePath;
static class PendingPermissionHandler {
final String path;
final Runnable onGranted;
final Runnable onDenied;
PendingPermissionHandler(@NonNull String path, @NonNull Runnable onGranted, @NonNull Runnable onDenied) {
this.path = path;
this.onGranted = onGranted;
this.onDenied = onDenied;
}