get result uri from edit intent
This commit is contained in:
parent
a0159c9b82
commit
28f7819eaf
5 changed files with 100 additions and 29 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue