Kotlin migration (WIP)

This commit is contained in:
Thibault Deckers 2020-10-21 15:12:10 +09:00
parent 87dc1768dd
commit 179fe36b8d
10 changed files with 277 additions and 288 deletions

View file

@ -1,107 +0,0 @@
package deckers.thibault.aves.channel.calls;
import android.content.Context;
import android.media.MediaScannerConnection;
import android.os.Build;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import deckers.thibault.aves.utils.PermissionManager;
import deckers.thibault.aves.utils.StorageUtils;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class StorageHandler implements MethodChannel.MethodCallHandler {
public static final String CHANNEL = "deckers.thibault/aves/storage";
private final Context context;
public StorageHandler(Context context) {
this.context = context;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "getStorageVolumes": {
List<Map<String, Object>> volumes;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
volumes = getStorageVolumes();
} else {
// TODO TLAD find alternative for Android <N
volumes = new ArrayList<>();
}
result.success(volumes);
break;
}
case "getInaccessibleDirectories": {
List<String> dirPaths = call.argument("dirPaths");
if (dirPaths == null) {
result.error("getInaccessibleDirectories-args", "failed because of missing arguments", null);
} else {
result.success(PermissionManager.getInaccessibleDirectories(context, dirPaths));
}
break;
}
case "revokeDirectoryAccess":
String path = call.argument("path");
if (path != null) {
PermissionManager.revokeDirectoryAccess(context, path);
}
result.success(true);
break;
case "getGrantedDirectories":
result.success(new ArrayList<>(PermissionManager.getGrantedDirs(context)));
break;
case "scanFile":
scanFile(call, new MethodResultWrapper(result));
break;
default:
result.notImplemented();
break;
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
private List<Map<String, Object>> getStorageVolumes() {
List<Map<String, Object>> volumes = new ArrayList<>();
StorageManager sm = context.getSystemService(StorageManager.class);
if (sm != null) {
for (String volumePath : StorageUtils.getVolumePaths(context)) {
try {
StorageVolume volume = sm.getStorageVolume(new File(volumePath));
if (volume != null) {
Map<String, Object> volumeMap = new HashMap<>();
volumeMap.put("path", volumePath);
volumeMap.put("description", volume.getDescription(context));
volumeMap.put("isPrimary", volume.isPrimary());
volumeMap.put("isRemovable", volume.isRemovable());
volumeMap.put("isEmulated", volume.isEmulated());
volumeMap.put("state", volume.getState());
volumes.add(volumeMap);
}
} catch (IllegalArgumentException e) {
// ignore
}
}
}
return volumes;
}
private void scanFile(MethodCall call, MethodChannel.Result result) {
String path = call.argument("path");
String mimeType = call.argument("mimeType");
MediaScannerConnection.scanFile(context, new String[]{path}, new String[]{mimeType}, (p, uri) -> {
result.success(uri != null ? uri.toString() : null);
});
}
}

View file

@ -1,164 +0,0 @@
package deckers.thibault.aves.channel.streams;
import android.app.Activity;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.FutureTarget;
import com.bumptech.glide.request.RequestOptions;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import deckers.thibault.aves.decoder.VideoThumbnail;
import deckers.thibault.aves.utils.BitmapUtils;
import deckers.thibault.aves.utils.MimeTypes;
import deckers.thibault.aves.utils.StorageUtils;
import io.flutter.plugin.common.EventChannel;
public class ImageByteStreamHandler implements EventChannel.StreamHandler {
public static final String CHANNEL = "deckers.thibault/aves/imagebytestream";
private Activity activity;
private Uri uri;
private String mimeType;
private int rotationDegrees;
private boolean isFlipped;
private EventChannel.EventSink eventSink;
private Handler handler;
@SuppressWarnings("unchecked")
public ImageByteStreamHandler(Activity activity, Object arguments) {
this.activity = activity;
if (arguments instanceof Map) {
Map<String, Object> argMap = (Map<String, Object>) arguments;
this.mimeType = (String) argMap.get("mimeType");
this.uri = Uri.parse((String) argMap.get("uri"));
this.rotationDegrees = (int) argMap.get("rotationDegrees");
this.isFlipped = (boolean) argMap.get("isFlipped");
}
}
@Override
public void onListen(Object args, EventChannel.EventSink eventSink) {
this.eventSink = eventSink;
this.handler = new Handler(Looper.getMainLooper());
new Thread(this::getImage).start();
}
@Override
public void onCancel(Object o) {
}
private void success(final byte[] bytes) {
handler.post(() -> eventSink.success(bytes));
}
private void error(final String errorCode, final String errorMessage, final Object errorDetails) {
handler.post(() -> eventSink.error(errorCode, errorMessage, errorDetails));
}
private void endOfStream() {
handler.post(() -> eventSink.endOfStream());
}
// Supported image formats:
// - Flutter (as of v1.20): JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP
// - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats
// - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java
private void getImage() {
// request a fresh image with the highest quality format
RequestOptions options = new RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true);
if (MimeTypes.isVideo(mimeType)) {
FutureTarget<Bitmap> target = Glide.with(activity)
.asBitmap()
.apply(options)
.load(new VideoThumbnail(activity, uri))
.submit();
try {
Bitmap bitmap = target.get();
if (bitmap != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
// Bitmap.CompressFormat.PNG is slower than JPEG
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
success(stream.toByteArray());
} else {
error("getImage-video-null", "failed to get image from uri=" + uri, null);
}
} catch (Exception e) {
error("getImage-video-exception", "failed to get image from uri=" + uri, e.getMessage());
} finally {
Glide.with(activity).clear(target);
}
} else if (!MimeTypes.isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
// we convert the image on platform side first, when Dart Image.memory does not support it
FutureTarget<Bitmap> target = Glide.with(activity)
.asBitmap()
.apply(options)
.load(uri)
.submit();
try {
Bitmap bitmap = target.get();
if (MimeTypes.needRotationAfterGlide(mimeType)) {
bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped);
}
if (bitmap != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
// Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency
if (MimeTypes.canHaveAlpha(mimeType)) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
} else {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
}
success(stream.toByteArray());
} else {
error("getImage-image-decode-null", "failed to get image from uri=" + uri, null);
}
} catch (Exception e) {
String errorDetails = e.getMessage();
if (errorDetails != null && !errorDetails.isEmpty()) {
errorDetails = errorDetails.split("\n", 2)[0];
}
error("getImage-image-decode-exception", "failed to get image from uri=" + uri, errorDetails);
} finally {
Glide.with(activity).clear(target);
}
} else {
try (InputStream is = StorageUtils.openInputStream(activity, uri)) {
if (is != null) {
streamBytes(is);
} else {
error("getImage-image-read-null", "failed to get image from uri=" + uri, null);
}
} catch (IOException e) {
error("getImage-image-read-exception", "failed to get image from uri=" + uri, e.getMessage());
}
}
endOfStream();
}
private void streamBytes(InputStream inputStream) throws IOException {
int bufferSize = 2 << 17; // 256kB
byte[] buffer = new byte[bufferSize];
int len;
while ((len = inputStream.read(buffer)) != -1) {
// cannot decode image on Flutter side when using `buffer` directly...
byte[] sub = new byte[len];
System.arraycopy(buffer, 0, sub, 0, len);
success(sub);
}
}
}

View file

@ -27,7 +27,7 @@ public class ImageOpStreamHandler implements EventChannel.StreamHandler {
public static final String CHANNEL = "deckers.thibault/aves/imageopstream"; public static final String CHANNEL = "deckers.thibault/aves/imageopstream";
private Context context; private final Context context;
private EventChannel.EventSink eventSink; private EventChannel.EventSink eventSink;
private Handler handler; private Handler handler;
private Map<String, Object> argMap; private Map<String, Object> argMap;

View file

@ -141,9 +141,9 @@ class MainActivity : FlutterActivity() {
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) { if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) {
val treeUri = data.data val treeUri = data?.data
if (resultCode != RESULT_OK || treeUri == null) { if (resultCode != RESULT_OK || treeUri == null) {
PermissionManager.onPermissionResult(requestCode, null) PermissionManager.onPermissionResult(requestCode, null)
return return

View file

@ -212,19 +212,19 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// simplify share intent for a single item, as some apps can handle one item but not more // simplify share intent for a single item, as some apps can handle one item but not more
if (uriList.size == 1) { if (uriList.size == 1) {
return shareSingle(title, uriList[0], mimeTypes[0]) return shareSingle(title, uriList.first(), mimeTypes.first())
} }
var mimeType = "*/*" var mimeType = "*/*"
if (mimeTypes.size == 1) { if (mimeTypes.size == 1) {
// items have the same mime type & subtype // items have the same mime type & subtype
mimeType = mimeTypes[0] mimeType = mimeTypes.first()
} else { } else {
// items have different subtypes // items have different subtypes
val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct() val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct()
if (mimeTypeTypes.size == 1) { if (mimeTypeTypes.size == 1) {
// items have the same mime type // items have the same mime type
mimeType = mimeTypeTypes[0].toString() + "/*" mimeType = mimeTypeTypes.first().toString() + "/*"
} }
} }

View file

@ -0,0 +1,94 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.storage.StorageManager
import androidx.annotation.RequiresApi
import deckers.thibault.aves.utils.PermissionManager
import deckers.thibault.aves.utils.StorageUtils.getVolumePaths
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import java.io.File
import java.util.*
class StorageHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getStorageVolumes" -> {
val volumes: List<Map<String, Any>> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
storageVolumes
} else {
// TODO TLAD find alternative for Android <N
emptyList()
}
result.success(volumes)
}
"getGrantedDirectories" -> result.success(ArrayList(PermissionManager.getGrantedDirs(context)))
"getInaccessibleDirectories" -> getInaccessibleDirectories(call, result)
"revokeDirectoryAccess" -> revokeDirectoryAccess(call, result)
"scanFile" -> scanFile(call, MethodResultWrapper(result))
else -> result.notImplemented()
}
}
private val storageVolumes: List<Map<String, Any>>
@RequiresApi(api = Build.VERSION_CODES.N)
get() {
val volumes = ArrayList<Map<String, Any>>()
val sm = context.getSystemService(StorageManager::class.java)
if (sm != null) {
for (volumePath in getVolumePaths(context)) {
try {
sm.getStorageVolume(File(volumePath))?.let {
val volumeMap: MutableMap<String, Any> = HashMap()
volumeMap["path"] = volumePath
volumeMap["description"] = it.getDescription(context)
volumeMap["isPrimary"] = it.isPrimary
volumeMap["isRemovable"] = it.isRemovable
volumeMap["isEmulated"] = it.isEmulated
volumeMap["state"] = it.state
volumes.add(volumeMap)
}
} catch (e: IllegalArgumentException) {
// ignore
}
}
}
return volumes
}
private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) {
val dirPaths = call.argument<List<String>>("dirPaths")
if (dirPaths == null) {
result.error("getInaccessibleDirectories-args", "failed because of missing arguments", null)
return
}
val dirs = PermissionManager.getInaccessibleDirectories(context, dirPaths)
result.success(dirs)
}
private fun revokeDirectoryAccess(call: MethodCall, result: MethodChannel.Result) {
val path = call.argument<String>("path")
if (path == null) {
result.error("revokeDirectoryAccess-args", "failed because of missing arguments", null)
return
}
val success = PermissionManager.revokeDirectoryAccess(context, path)
result.success(success)
}
private fun scanFile(call: MethodCall, result: MethodChannel.Result) {
val path = call.argument<String>("path")
val mimeType = call.argument<String>("mimeType")
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, uri: Uri? -> result.success(uri?.toString()) }
}
companion object {
const val CHANNEL = "deckers.thibault/aves/storage"
}
}

View file

@ -0,0 +1,169 @@
package deckers.thibault.aves.channel.streams
import android.app.Activity
import android.graphics.Bitmap
import android.net.Uri
import android.os.Handler
import android.os.Looper
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.decoder.VideoThumbnail
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
import deckers.thibault.aves.utils.MimeTypes.canHaveAlpha
import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import deckers.thibault.aves.utils.StorageUtils.openInputStream
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
class ImageByteStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
private lateinit var eventSink: EventSink
private lateinit var handler: Handler
override fun onListen(args: Any, eventSink: EventSink) {
this.eventSink = eventSink
handler = Handler(Looper.getMainLooper())
Thread { streamImage() }.start()
}
override fun onCancel(o: Any) {}
private fun success(bytes: ByteArray) {
handler.post { eventSink.success(bytes) }
}
private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) {
handler.post { eventSink.error(errorCode, errorMessage, errorDetails) }
}
private fun endOfStream() {
handler.post { eventSink.endOfStream() }
}
// Supported image formats:
// - Flutter (as of v1.20): JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP
// - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats
// - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java
private fun streamImage() {
if (arguments !is Map<*, *>) {
endOfStream()
return
}
val mimeType = arguments["mimeType"] as String?
val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) }
val rotationDegrees = arguments["rotationDegrees"] as Int
val isFlipped = arguments["isFlipped"] as Boolean
if (mimeType == null || uri == null) {
error("streamImage-args", "failed because of missing arguments", null)
endOfStream()
return
}
if (isVideo(mimeType)) {
streamVideoByGlide(uri)
} else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
// decode exotic format on platform side, then encode it in portable format for Flutter
streamImageByGlide(uri, mimeType, rotationDegrees, isFlipped)
} else {
// to be decoded by Flutter
streamImageAsIs(uri)
}
endOfStream()
}
private fun streamImageAsIs(uri: Uri) {
try {
openInputStream(activity, uri).use { input -> input?.let { streamBytes(it) } }
} catch (e: IOException) {
error("streamImage-image-read-exception", "failed to get image from uri=$uri", e.message)
}
}
private fun streamImageByGlide(uri: Uri, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) {
val target = Glide.with(activity)
.asBitmap()
.apply(options)
.load(uri)
.submit()
try {
var bitmap = target.get()
if (needRotationAfterGlide(mimeType)) {
bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped)
}
if (bitmap != null) {
val stream = ByteArrayOutputStream()
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
// Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency
if (canHaveAlpha(mimeType)) {
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
} else {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
}
success(stream.toByteArray())
} else {
error("streamImage-image-decode-null", "failed to get image from uri=$uri", null)
}
} catch (e: Exception) {
var errorDetails = e.message
if (errorDetails?.isNotEmpty() == true) {
errorDetails = errorDetails.split("\n".toRegex(), 2).first()
}
error("streamImage-image-decode-exception", "failed to get image from uri=$uri", errorDetails)
} finally {
Glide.with(activity).clear(target)
}
}
private fun streamVideoByGlide(uri: Uri) {
val target = Glide.with(activity)
.asBitmap()
.apply(options)
.load(VideoThumbnail(activity, uri))
.submit()
try {
val bitmap = target.get()
if (bitmap != null) {
val stream = ByteArrayOutputStream()
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
// Bitmap.CompressFormat.PNG is slower than JPEG
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
success(stream.toByteArray())
} else {
error("streamImage-video-null", "failed to get image from uri=$uri", null)
}
} catch (e: Exception) {
error("streamImage-video-exception", "failed to get image from uri=$uri", e.message)
} finally {
Glide.with(activity).clear(target)
}
}
private fun streamBytes(inputStream: InputStream) {
val buffer = ByteArray(bufferSize)
var len: Int
while (inputStream.read(buffer).also { len = it } != -1) {
// cannot decode image on Flutter side when using `buffer` directly
success(buffer.copyOf(len))
}
}
companion object {
const val CHANNEL = "deckers.thibault/aves/imagebytestream"
const val bufferSize = 2 shl 17 // 256kB
// request a fresh image with the highest quality format
val options = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
}
}

View file

@ -11,6 +11,7 @@ import io.flutter.plugin.common.EventChannel.EventSink
class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : EventChannel.StreamHandler { class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : EventChannel.StreamHandler {
private lateinit var eventSink: EventSink private lateinit var eventSink: EventSink
private lateinit var handler: Handler private lateinit var handler: Handler
private var knownEntries: Map<Int, Int?>? = null private var knownEntries: Map<Int, Int?>? = null
init { init {

View file

@ -53,10 +53,10 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
if (picture == null) { if (picture == null) {
// not ideal: bitmap -> byte[] -> bitmap // not ideal: bitmap -> byte[] -> bitmap
// but simple fallback and we cache result // but simple fallback and we cache result
val bos = ByteArrayOutputStream() val stream = ByteArrayOutputStream()
val bitmap = retriever.frameAtTime val bitmap = retriever.frameAtTime
bitmap?.compress(Bitmap.CompressFormat.PNG, 0, bos) bitmap?.compress(Bitmap.CompressFormat.PNG, 0, stream)
picture = bos.toByteArray() picture = stream.toByteArray()
} }
callback.onDataReady(ByteArrayInputStream(picture)) callback.onDataReady(ByteArrayInputStream(picture))
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -22,7 +22,6 @@ object PermissionManager {
// permission request code to pending runnable // permission request code to pending runnable
private val pendingPermissionMap = ConcurrentHashMap<Int, PendingPermissionHandler>() private val pendingPermissionMap = ConcurrentHashMap<Int, PendingPermissionHandler>()
@JvmStatic
fun requestVolumeAccess(activity: Activity, path: String, onGranted: () -> Unit, onDenied: () -> Unit) { fun requestVolumeAccess(activity: Activity, path: String, onGranted: () -> Unit, onDenied: () -> Unit) {
Log.i(LOG_TAG, "request user to select and grant access permission to volume=$path") Log.i(LOG_TAG, "request user to select and grant access permission to volume=$path")
pendingPermissionMap[VOLUME_ACCESS_REQUEST_CODE] = PendingPermissionHandler(path, onGranted, onDenied) pendingPermissionMap[VOLUME_ACCESS_REQUEST_CODE] = PendingPermissionHandler(path, onGranted, onDenied)
@ -47,12 +46,10 @@ object PermissionManager {
(if (treeUri != null) handler.onGranted else handler.onDenied)() (if (treeUri != null) handler.onGranted else handler.onDenied)()
} }
@JvmStatic
fun getGrantedDirForPath(context: Context, anyPath: String): String? { fun getGrantedDirForPath(context: Context, anyPath: String): String? {
return getAccessibleDirs(context).firstOrNull { anyPath.startsWith(it) } return getAccessibleDirs(context).firstOrNull { anyPath.startsWith(it) }
} }
@JvmStatic
fun getInaccessibleDirectories(context: Context, dirPaths: List<String>): List<Map<String, String>> { fun getInaccessibleDirectories(context: Context, dirPaths: List<String>): List<Map<String, String>> {
val accessibleDirs = getAccessibleDirs(context) val accessibleDirs = getAccessibleDirs(context)
@ -103,16 +100,15 @@ object PermissionManager {
return inaccessibleDirs return inaccessibleDirs
} }
@JvmStatic fun revokeDirectoryAccess(context: Context, path: String): Boolean {
fun revokeDirectoryAccess(context: Context, path: String) { return StorageUtils.convertDirPathToTreeUri(context, path)?.let {
StorageUtils.convertDirPathToTreeUri(context, path)?.let {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.releasePersistableUriPermission(it, flags) context.contentResolver.releasePersistableUriPermission(it, flags)
} true
} ?: false
} }
// returns paths matching URIs granted by the user // returns paths matching URIs granted by the user
@JvmStatic
fun getGrantedDirs(context: Context): Set<String> { fun getGrantedDirs(context: Context): Set<String> {
val grantedDirs = HashSet<String>() val grantedDirs = HashSet<String>()
for (uriPermission in context.contentResolver.persistedUriPermissions) { for (uriPermission in context.contentResolver.persistedUriPermissions) {