get result uri from edit intent

This commit is contained in:
Thibault Deckers 2024-02-11 19:45:52 +01:00
parent a0159c9b82
commit 28f7819eaf
5 changed files with 100 additions and 29 deletions

View file

@ -21,6 +21,7 @@ import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.* import deckers.thibault.aves.channel.streams.*
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
import deckers.thibault.aves.utils.FlutterUtils.isSoftwareRenderingRequired import deckers.thibault.aves.utils.FlutterUtils.isSoftwareRenderingRequired
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
@ -218,6 +219,7 @@ open class MainActivity : FlutterFragmentActivity() {
OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data) OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data)
PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data) PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data)
EDIT_REQUEST -> onEditResult(resultCode, data)
} }
} }
@ -226,6 +228,14 @@ open class MainActivity : FlutterFragmentActivity() {
pendingCollectionFilterPickHandler?.let { it(filters) } pendingCollectionFilterPickHandler?.let { it(filters) }
} }
private fun onEditResult(resultCode: Int, intent: Intent?) {
val fields: FieldMap? = if (resultCode == RESULT_OK) hashMapOf(
"uri" to intent?.data.toString(),
"mimeType" to intent?.type,
) else null
pendingEditIntentHandler?.let { it(fields) }
}
private fun onDocumentTreeAccessResult(requestCode: Int, resultCode: Int, intent: Intent?) { private fun onDocumentTreeAccessResult(requestCode: Int, resultCode: Int, intent: Intent?) {
val treeUri = intent?.data val treeUri = intent?.data
if (resultCode != RESULT_OK || treeUri == null) { if (resultCode != RESULT_OK || treeUri == null) {
@ -458,6 +468,7 @@ open class MainActivity : FlutterFragmentActivity() {
const val DELETE_SINGLE_PERMISSION_REQUEST = 5 const val DELETE_SINGLE_PERMISSION_REQUEST = 5
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6 const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6
const val PICK_COLLECTION_FILTERS_REQUEST = 7 const val PICK_COLLECTION_FILTERS_REQUEST = 7
const val EDIT_REQUEST = 8
const val INTENT_ACTION_EDIT = "edit" const val INTENT_ACTION_EDIT = "edit"
const val INTENT_ACTION_PICK_ITEMS = "pick_items" const val INTENT_ACTION_PICK_ITEMS = "pick_items"
@ -493,6 +504,8 @@ open class MainActivity : FlutterFragmentActivity() {
var pendingCollectionFilterPickHandler: ((filters: List<String>?) -> Unit)? = null var pendingCollectionFilterPickHandler: ((filters: List<String>?) -> Unit)? = null
var pendingEditIntentHandler: ((fields: FieldMap?) -> Unit)? = null
private fun onStorageAccessResult(requestCode: Int, uri: Uri?) { private fun onStorageAccessResult(requestCode: Int, uri: Uri?) {
Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri") Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri")
val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return

View file

@ -52,7 +52,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
"getPackages" -> ioScope.launch { safe(call, result, ::getPackages) } "getPackages" -> ioScope.launch { safe(call, result, ::getPackages) }
"getAppIcon" -> ioScope.launch { safeSuspend(call, result, ::getAppIcon) } "getAppIcon" -> ioScope.launch { safeSuspend(call, result, ::getAppIcon) }
"copyToClipboard" -> ioScope.launch { safe(call, result, ::copyToClipboard) } "copyToClipboard" -> ioScope.launch { safe(call, result, ::copyToClipboard) }
"edit" -> safe(call, result, ::edit)
"open" -> safe(call, result, ::open) "open" -> safe(call, result, ::open)
"openMap" -> safe(call, result, ::openMap) "openMap" -> safe(call, result, ::openMap)
"setAs" -> safe(call, result, ::setAs) "setAs" -> safe(call, result, ::setAs)
@ -207,22 +206,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} }
} }
private fun edit(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType")
if (uri == null) {
result.error("edit-args", "missing arguments", null)
return
}
val intent = Intent(Intent.ACTION_EDIT)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.setDataAndType(getShareableUri(context, uri), mimeType)
val started = safeStartActivity(intent)
result.success(started)
}
private fun open(call: MethodCall, result: MethodChannel.Result) { private fun open(call: MethodCall, result: MethodChannel.Result) {
val title = call.argument<String>("title") val title = call.argument<String>("title")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
@ -404,6 +387,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut // on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback // so we use a joined `String` as fallback
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR)) .putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
else -> { else -> {
result.error("pin-intent", "failed to build intent", null) result.error("pin-intent", "failed to build intent", null)
return return
@ -434,6 +418,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
FileProvider.getUriForFile(context, authority, File(path)) FileProvider.getUriForFile(context, authority, File(path))
} }
} }
else -> uri else -> uri
} }
} }

View file

@ -9,6 +9,7 @@ import android.os.Looper
import android.util.Log import android.util.Log
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.PendingStorageAccessResultHandler import deckers.thibault.aves.PendingStorageAccessResultHandler
import deckers.thibault.aves.channel.calls.AppAdapterHandler
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.PermissionManager import deckers.thibault.aves.utils.PermissionManager
@ -47,6 +48,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() } "requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
"createFile" -> ioScope.launch { createFile() } "createFile" -> ioScope.launch { createFile() }
"openFile" -> ioScope.launch { openFile() } "openFile" -> ioScope.launch { openFile() }
"edit" -> edit()
"pickCollectionFilters" -> pickCollectionFilters() "pickCollectionFilters" -> pickCollectionFilters()
else -> endOfStream() else -> endOfStream()
} }
@ -100,10 +102,13 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
endOfStream() endOfStream()
} }
private suspend fun safeStartActivityForResult(intent: Intent, requestCode: Int, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) { private suspend fun safeStartActivityForStorageAccessResult(intent: Intent, requestCode: Int, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) {
if (intent.resolveActivity(activity.packageManager) != null) { if (intent.resolveActivity(activity.packageManager) != null) {
MainActivity.pendingStorageAccessResultHandlers[requestCode] = PendingStorageAccessResultHandler(null, onGranted, onDenied) MainActivity.pendingStorageAccessResultHandlers[requestCode] = PendingStorageAccessResultHandler(null, onGranted, onDenied)
activity.startActivityForResult(intent, requestCode) if (!safeStartActivityForResult(intent, requestCode)) {
MainActivity.notifyError("failed to start activity for intent=$intent extras=${intent.extras}")
onDenied()
}
} else { } else {
MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}") MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
onDenied() onDenied()
@ -144,7 +149,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
type = mimeType type = mimeType
putExtra(Intent.EXTRA_TITLE, name) putExtra(Intent.EXTRA_TITLE, name)
} }
safeStartActivityForResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied) safeStartActivityForStorageAccessResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied)
} }
private suspend fun openFile() { private suspend fun openFile() {
@ -177,7 +182,33 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
setTypeAndNormalize(mimeType ?: MimeTypes.ANY) setTypeAndNormalize(mimeType ?: MimeTypes.ANY)
} }
safeStartActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied) safeStartActivityForStorageAccessResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied)
}
private fun edit() {
val uri = args["uri"] as String?
val mimeType = args["mimeType"] as String? // optional
if (uri == null) {
error("edit-args", "missing arguments", null)
return
}
val intent = Intent(Intent.ACTION_EDIT)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.setDataAndType(AppAdapterHandler.getShareableUri(activity, Uri.parse(uri)), mimeType)
if (intent.resolveActivity(activity.packageManager) == null) {
error("edit-resolve", "cannot resolve activity for this intent", null)
return
}
MainActivity.pendingEditIntentHandler = { fields ->
success(fields)
endOfStream()
}
if (!safeStartActivityForResult(intent, MainActivity.EDIT_REQUEST)) {
error("edit-start", "cannot start activity for this intent", null)
}
} }
private fun pickCollectionFilters() { private fun pickCollectionFilters() {
@ -192,6 +223,24 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST) activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST)
} }
private fun safeStartActivityForResult(intent: Intent, requestCode: Int): Boolean {
return try {
activity.startActivityForResult(intent, requestCode)
true
} catch (e: SecurityException) {
if (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION != 0) {
// in some environments, providing the write flag yields a `SecurityException`:
// "UID XXXX does not have permission to content://XXXX"
// so we retry without it
Log.i(LOG_TAG, "retry intent=$intent without FLAG_GRANT_WRITE_URI_PERMISSION")
intent.flags = intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION.inv()
safeStartActivityForResult(intent, requestCode)
} else {
false
}
}
}
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments") Log.i(LOG_TAG, "onCancel arguments=$arguments")
} }

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:aves/model/apps.dart'; import 'package:aves/model/apps.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
@ -7,6 +9,7 @@ import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:streams_channel/streams_channel.dart';
abstract class AppService { abstract class AppService {
Future<Set<Package>> getPackages(); Future<Set<Package>> getPackages();
@ -15,7 +18,7 @@ abstract class AppService {
Future<bool> copyToClipboard(String uri, String? label); Future<bool> copyToClipboard(String uri, String? label);
Future<bool> edit(String uri, String mimeType); Future<Map<String, dynamic>> edit(String uri, String mimeType);
Future<bool> open(String uri, String mimeType, {required bool forceChooser}); Future<bool> open(String uri, String mimeType, {required bool forceChooser});
@ -32,6 +35,7 @@ abstract class AppService {
class PlatformAppService implements AppService { class PlatformAppService implements AppService {
static const _platform = MethodChannel('deckers.thibault/aves/app'); static const _platform = MethodChannel('deckers.thibault/aves/app');
static final _stream = StreamsChannel('deckers.thibault/aves/activity_result_stream');
static final _knownAppDirs = { static final _knownAppDirs = {
'com.kakao.talk': {'KakaoTalkDownload'}, 'com.kakao.talk': {'KakaoTalkDownload'},
@ -89,17 +93,29 @@ class PlatformAppService implements AppService {
} }
@override @override
Future<bool> edit(String uri, String mimeType) async { Future<Map<String, dynamic>> edit(String uri, String mimeType) async {
try { try {
final result = await _platform.invokeMethod('edit', <String, dynamic>{ final completer = Completer<Map?>();
_stream.receiveBroadcastStream(<String, dynamic>{
'op': 'edit',
'uri': uri, 'uri': uri,
'mimeType': mimeType, 'mimeType': mimeType,
}); }).listen(
if (result != null) return result as bool; (data) => completer.complete(data as Map?),
onError: completer.completeError,
onDone: () {
if (!completer.isCompleted) completer.complete({'error': 'cancelled'});
},
cancelOnError: true,
);
// `await` here, so that `completeError` will be caught below
final result = await completer.future;
if (result == null) return {'error': 'cancelled'};
return result.cast<String, dynamic>();
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
return {'error': e.code};
} }
return false;
} }
@override @override

View file

@ -242,8 +242,16 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
).dispatch(context); ).dispatch(context);
} }
case EntryAction.edit: case EntryAction.edit:
appService.edit(targetEntry.uri, targetEntry.mimeType).then((success) { appService.edit(targetEntry.uri, targetEntry.mimeType).then((fields) {
if (!success) showNoMatchingAppDialog(context); final error = fields['error'] as String?;
if (error == null) {
final uri = fields['uri'] as String?;
if (uri != null) {
debugPrint('TLAD uri=$uri');
}
} else if (error == 'edit-resolve') {
showNoMatchingAppDialog(context);
}
}); });
case EntryAction.open: case EntryAction.open:
appService.open(targetEntry.uri, targetEntry.mimeTypeAnySubtype, forceChooser: true).then((success) { appService.open(targetEntry.uri, targetEntry.mimeTypeAnySubtype, forceChooser: true).then((success) {