API 30: prep to request access by directory, not volume
This commit is contained in:
parent
ffc989d9a3
commit
51fb36bb70
5 changed files with 37 additions and 67 deletions
|
@ -100,7 +100,7 @@ public class MainActivity extends FlutterActivity {
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
if (requestCode == PermissionManager.VOLUME_ROOT_PERMISSION_REQUEST_CODE) {
|
if (requestCode == PermissionManager.VOLUME_ROOT_PERMISSION_REQUEST_CODE) {
|
||||||
if (resultCode != RESULT_OK || data.getData() == null) {
|
if (resultCode != RESULT_OK || data.getData() == null) {
|
||||||
PermissionManager.onPermissionResult(this, requestCode, false, null);
|
PermissionManager.onPermissionResult(this, requestCode, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ public class MainActivity extends FlutterActivity {
|
||||||
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
|
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
|
||||||
|
|
||||||
// resume pending action
|
// resume pending action
|
||||||
PermissionManager.onPermissionResult(this, requestCode, true, treeUri);
|
PermissionManager.onPermissionResult(this, requestCode, treeUri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,14 @@ public class StorageAccessStreamHandler implements EventChannel.StreamHandler {
|
||||||
private Activity activity;
|
private Activity activity;
|
||||||
private EventChannel.EventSink eventSink;
|
private EventChannel.EventSink eventSink;
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
private String volumePath;
|
private String path;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public StorageAccessStreamHandler(Activity activity, Object arguments) {
|
public StorageAccessStreamHandler(Activity activity, Object arguments) {
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
if (arguments instanceof Map) {
|
if (arguments instanceof Map) {
|
||||||
Map<String, Object> argMap = (Map<String, Object>) arguments;
|
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) {
|
public void onListen(Object o, final EventChannel.EventSink eventSink) {
|
||||||
this.eventSink = eventSink;
|
this.eventSink = eventSink;
|
||||||
this.handler = new Handler(Looper.getMainLooper());
|
this.handler = new Handler(Looper.getMainLooper());
|
||||||
Runnable onGranted = () -> success(!PermissionManager.requireVolumeAccessDialog(activity, volumePath));
|
Runnable onGranted = () -> success(!PermissionManager.requireVolumeAccessDialog(activity, path));
|
||||||
Runnable onDenied = () -> success(false);
|
Runnable onDenied = () -> success(false);
|
||||||
PermissionManager.requestVolumeAccess(activity, volumePath, onGranted, onDenied);
|
PermissionManager.requestVolumeAccess(activity, path, onGranted, onDenied);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -9,8 +9,6 @@ import android.graphics.BitmapFactory;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
import android.media.MediaScannerConnection;
|
import android.media.MediaScannerConnection;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
@ -32,7 +30,6 @@ import java.util.Map;
|
||||||
import deckers.thibault.aves.model.AvesImageEntry;
|
import deckers.thibault.aves.model.AvesImageEntry;
|
||||||
import deckers.thibault.aves.utils.MetadataHelper;
|
import deckers.thibault.aves.utils.MetadataHelper;
|
||||||
import deckers.thibault.aves.utils.MimeTypes;
|
import deckers.thibault.aves.utils.MimeTypes;
|
||||||
import deckers.thibault.aves.utils.PermissionManager;
|
|
||||||
import deckers.thibault.aves.utils.StorageUtils;
|
import deckers.thibault.aves.utils.StorageUtils;
|
||||||
import deckers.thibault.aves.utils.Utils;
|
import deckers.thibault.aves.utils.Utils;
|
||||||
|
|
||||||
|
@ -74,12 +71,6 @@ public abstract class ImageProvider {
|
||||||
return;
|
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);
|
DocumentFileCompat df = StorageUtils.getDocumentFile(activity, oldPath, oldMediaUri);
|
||||||
try {
|
try {
|
||||||
boolean renamed = df != null && df.renameTo(newFilename);
|
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) {
|
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) {
|
switch (mimeType) {
|
||||||
case MimeTypes.JPEG:
|
case MimeTypes.JPEG:
|
||||||
rotateJpeg(activity, path, uri, clockwise, callback);
|
rotateJpeg(activity, path, uri, clockwise, callback);
|
||||||
|
|
|
@ -8,8 +8,6 @@ import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.os.storage.StorageManager;
|
import android.os.storage.StorageManager;
|
||||||
import android.os.storage.StorageVolume;
|
import android.os.storage.StorageVolume;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
|
@ -35,7 +33,6 @@ import java.util.stream.Stream;
|
||||||
import deckers.thibault.aves.model.AvesImageEntry;
|
import deckers.thibault.aves.model.AvesImageEntry;
|
||||||
import deckers.thibault.aves.model.SourceImageEntry;
|
import deckers.thibault.aves.model.SourceImageEntry;
|
||||||
import deckers.thibault.aves.utils.MimeTypes;
|
import deckers.thibault.aves.utils.MimeTypes;
|
||||||
import deckers.thibault.aves.utils.PermissionManager;
|
|
||||||
import deckers.thibault.aves.utils.StorageUtils;
|
import deckers.thibault.aves.utils.StorageUtils;
|
||||||
import deckers.thibault.aves.utils.Utils;
|
import deckers.thibault.aves.utils.Utils;
|
||||||
|
|
||||||
|
@ -217,18 +214,6 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
SettableFuture<Object> future = SettableFuture.create();
|
SettableFuture<Object> future = SettableFuture.create();
|
||||||
|
|
||||||
if (StorageUtils.requireAccessPermission(path)) {
|
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
|
// 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
|
// but it doesn't delete the file, even if the app has the permission
|
||||||
try {
|
try {
|
||||||
|
@ -276,12 +261,6 @@ public class MediaStoreImageProvider extends ImageProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void moveMultiple(final Activity activity, final Boolean copy, final String destinationDir, final List<AvesImageEntry> entries, @NonNull final ImageOpCallback callback) {
|
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);
|
DocumentFileCompat destinationDirDocFile = StorageUtils.createDirectoryIfAbsent(activity, destinationDir);
|
||||||
if (destinationDirDocFile == null) {
|
if (destinationDirDocFile == null) {
|
||||||
callback.onFailure(new Exception("failed to create directory at path=" + destinationDir));
|
callback.onFailure(new Exception("failed to create directory at path=" + destinationDir));
|
||||||
|
|
|
@ -41,26 +41,15 @@ public class PermissionManager {
|
||||||
return uriPermissionOptional.map(UriPermission::getUri).orElse(null);
|
return uriPermissionOptional.map(UriPermission::getUri).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
public static void requestVolumeAccess(@NonNull Activity activity, @NonNull String path, @NonNull Runnable onGranted, @NonNull Runnable onDenied) {
|
||||||
public static void showVolumeAccessDialog(final Activity activity, @NonNull String anyPath, final Runnable pendingRunnable) {
|
Log.i(LOG_TAG, "request user to select and grant access permission to volume=" + path);
|
||||||
String volumePath = StorageUtils.getVolumePath(activity, anyPath).orElse(null);
|
pendingPermissionMap.put(VOLUME_ROOT_PERMISSION_REQUEST_CODE, new PendingPermissionHandler(path, onGranted, onDenied));
|
||||||
// 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));
|
|
||||||
|
|
||||||
Intent intent = null;
|
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);
|
StorageManager sm = activity.getSystemService(StorageManager.class);
|
||||||
if (sm != null) {
|
if (sm != null) {
|
||||||
StorageVolume volume = sm.getStorageVolume(new File(volumePath));
|
StorageVolume volume = sm.getStorageVolume(new File(path));
|
||||||
if (volume != null) {
|
if (volume != null) {
|
||||||
intent = volume.createOpenDocumentTreeIntent();
|
intent = volume.createOpenDocumentTreeIntent();
|
||||||
}
|
}
|
||||||
|
@ -75,25 +64,42 @@ public class PermissionManager {
|
||||||
ActivityCompat.startActivityForResult(activity, intent, VOLUME_ROOT_PERMISSION_REQUEST_CODE, null);
|
ActivityCompat.startActivityForResult(activity, intent, VOLUME_ROOT_PERMISSION_REQUEST_CODE, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void onPermissionResult(Activity activity, int requestCode, boolean granted, Uri treeUri) {
|
public static void onPermissionResult(Activity activity, int requestCode, @Nullable Uri treeUri) {
|
||||||
Log.d(LOG_TAG, "onPermissionResult with requestCode=" + requestCode + ", granted=" + granted);
|
Log.d(LOG_TAG, "onPermissionResult with requestCode=" + requestCode + ", treeUri=" + treeUri);
|
||||||
|
boolean granted = treeUri != null;
|
||||||
|
|
||||||
PendingPermissionHandler handler = pendingPermissionMap.remove(requestCode);
|
PendingPermissionHandler handler = pendingPermissionMap.remove(requestCode);
|
||||||
if (handler == null) return;
|
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;
|
Runnable runnable = granted ? handler.onGranted : handler.onDenied;
|
||||||
if (runnable == null) return;
|
if (runnable == null) return;
|
||||||
runnable.run();
|
runnable.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
static class PendingPermissionHandler {
|
private static boolean isTreeUriPath(String path, Uri treeUri) {
|
||||||
String volumePath;
|
// TODO TLAD check requestedPath match treeUri
|
||||||
Runnable onGranted;
|
// e.g. OK match for path=/storage/emulated/0/, treeUri=content://com.android.externalstorage.documents/tree/primary%3A
|
||||||
Runnable onDenied;
|
// 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) {
|
static class PendingPermissionHandler {
|
||||||
this.volumePath = volumePath;
|
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.onGranted = onGranted;
|
||||||
this.onDenied = onDenied;
|
this.onDenied = onDenied;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue