From f385f1620958e3eb8d16d492b9e8259bb6d7f40d Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 8 Nov 2021 14:35:45 +0900 Subject: [PATCH] improved error reporting when requesting directory permission --- .../deckers/thibault/aves/AnalysisService.kt | 6 ++--- .../deckers/thibault/aves/MainActivity.kt | 5 +++- .../aves/SearchSuggestionsProvider.kt | 3 +-- .../channel/streams/ErrorStreamHandler.kt | 7 ++++-- .../streams/StorageAccessStreamHandler.kt | 2 +- .../model/provider/MediaStoreImageProvider.kt | 1 + .../thibault/aves/utils/ContextUtils.kt | 17 -------------- .../thibault/aves/utils/FlutterUtils.kt | 23 ++++++++++++++++--- .../thibault/aves/utils/PermissionManager.kt | 11 ++++++--- 9 files changed, 42 insertions(+), 33 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt index c35efa712..d4cfcb806 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisService.kt @@ -17,7 +17,6 @@ import deckers.thibault.aves.channel.calls.MediaStoreHandler import deckers.thibault.aves.channel.calls.MetadataFetchHandler import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler -import deckers.thibault.aves.utils.ContextUtils.runOnUiThread import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.LogUtils import io.flutter.embedding.engine.FlutterEngine @@ -155,12 +154,11 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() { private inner class ServiceHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { - val context = this@AnalysisService val data = msg.data when (data.getString(KEY_COMMAND)) { COMMAND_START -> { runBlocking { - context.runOnUiThread { + FlutterUtils.runOnUiThread { val contentIds = data.get(KEY_CONTENT_IDS)?.takeIf { it is IntArray }?.let { (it as IntArray).toList() } backgroundChannel?.invokeMethod( "start", hashMapOf( @@ -174,7 +172,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() { COMMAND_STOP -> { // unconditionally stop the service runBlocking { - context.runOnUiThread { + FlutterUtils.runOnUiThread { backgroundChannel?.invokeMethod("stop", null) } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index 9dd4032e5..64cba6a9c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -311,7 +311,10 @@ class MainActivity : FlutterActivity() { var errorStreamHandler: ErrorStreamHandler? = null - fun notifyError(error: String) = errorStreamHandler?.notifyError(error) + suspend fun notifyError(error: String) { + Log.e(LOG_TAG, "notifyError error=$error") + errorStreamHandler?.notifyError(error) + } } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt index bb967a4a8..ca9ba2fa7 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/SearchSuggestionsProvider.kt @@ -12,7 +12,6 @@ import android.text.format.DateFormat import android.util.Log import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.ContextUtils.resourceUri -import deckers.thibault.aves.utils.ContextUtils.runOnUiThread import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.LogUtils import io.flutter.embedding.engine.FlutterEngine @@ -80,7 +79,7 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid return suspendCoroutine { cont -> GlobalScope.launch { - context.runOnUiThread { + FlutterUtils.runOnUiThread { backgroundChannel.invokeMethod("getSuggestions", hashMapOf( "query" to query, "locale" to Locale.getDefault().toString(), diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt index b5eab9563..1896fd0ef 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt @@ -1,5 +1,6 @@ package deckers.thibault.aves.channel.streams +import deckers.thibault.aves.utils.FlutterUtils import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink @@ -15,8 +16,10 @@ class ErrorStreamHandler : EventChannel.StreamHandler { override fun onCancel(arguments: Any?) {} - fun notifyError(error: String) { - eventSink?.success(error) + suspend fun notifyError(error: String) { + FlutterUtils.runOnUiThread { + eventSink?.success(error) + } } companion object { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt index 4a63445bf..6586c5df8 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt @@ -49,7 +49,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? } } - private fun requestDirectoryAccess() { + private suspend fun requestDirectoryAccess() { val path = args["path"] as String? if (path == null) { error("requestDirectoryAccess-args", "failed because of missing arguments", null) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index 487d04f8f..2d3e62f9e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -323,6 +323,7 @@ class MediaStoreImageProvider : ImageProvider() { // with a path, and retrieve its content URI, but: // - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`) // - the volume name should be lower case, not exactly as the `StorageVolume` UUID + // cf new method in API 30 `StorageVolume.getMediaStoreVolumeName()` // - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?) // - there is no documentation regarding support for usage with removable storage // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/ContextUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/ContextUtils.kt index d31dd5c26..7b4d57941 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/ContextUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/ContextUtils.kt @@ -5,10 +5,6 @@ import android.app.Service import android.content.ContentResolver import android.content.Context import android.net.Uri -import android.os.Handler -import android.os.Looper -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine object ContextUtils { fun Context.resourceUri(resourceId: Int): Uri = with(resources) { @@ -20,19 +16,6 @@ object ContextUtils { .build() } - suspend fun Context.runOnUiThread(r: Runnable) { - if (Looper.myLooper() != mainLooper) { - suspendCoroutine { cont -> - Handler(mainLooper).post { - r.run() - cont.resume(true) - } - } - } else { - r.run() - } - } - fun Context.isMyServiceRunning(serviceClass: Class): Boolean { val am = this.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? am ?: return false diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt index 98a1dbed6..973743105 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/FlutterUtils.kt @@ -1,13 +1,16 @@ package deckers.thibault.aves.utils import android.content.Context +import android.os.Handler +import android.os.Looper import android.util.Log -import deckers.thibault.aves.utils.ContextUtils.runOnUiThread import io.flutter.FlutterInjector import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.loader.FlutterLoader import io.flutter.view.FlutterCallbackInformation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine object FlutterUtils { private val LOG_TAG = LogUtils.createTag() @@ -20,7 +23,7 @@ object FlutterUtils { } lateinit var flutterLoader: FlutterLoader - context.runOnUiThread { + FlutterUtils.runOnUiThread { // initialization must happen on the main thread flutterLoader = FlutterInjector.instance().flutterLoader().apply { startInitialization(context) @@ -39,11 +42,25 @@ object FlutterUtils { flutterLoader.findAppBundlePath(), callbackInfo ) - context.runOnUiThread { + runOnUiThread { val engine = FlutterEngine(context).apply { dartExecutor.executeDartCallback(args) } engineSetter(engine) } } + + suspend fun runOnUiThread(r: Runnable) { + val mainLooper = Looper.getMainLooper() + if (Looper.myLooper() != mainLooper) { + suspendCoroutine { cont -> + Handler(mainLooper).post { + r.run() + cont.resume(true) + } + } + } else { + r.run() + } + } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt index 1655f9430..32c40d1da 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt @@ -31,13 +31,18 @@ object PermissionManager { ) @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - fun requestDirectoryAccess(activity: Activity, path: String, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) { + suspend fun requestDirectoryAccess(activity: Activity, path: String, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) { Log.i(LOG_TAG, "request user to select and grant access permission to path=$path") var intent: Intent? = null if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val sm = activity.getSystemService(Context.STORAGE_SERVICE) as? StorageManager - intent = sm?.getStorageVolume(File(path))?.createOpenDocumentTreeIntent() + val storageVolume = sm?.getStorageVolume(File(path)) + if (storageVolume != null) { + intent = storageVolume.createOpenDocumentTreeIntent() + } else { + MainActivity.notifyError("failed to get storage volume for path=$path on volumes=${sm?.storageVolumes?.joinToString(", ")}") + } } // fallback to basic open document tree intent @@ -49,7 +54,7 @@ object PermissionManager { MainActivity.pendingStorageAccessResultHandlers[MainActivity.DOCUMENT_TREE_ACCESS_REQUEST] = PendingStorageAccessResultHandler(path, onGranted, onDenied) activity.startActivityForResult(intent, MainActivity.DOCUMENT_TREE_ACCESS_REQUEST) } else { - Log.e(LOG_TAG, "failed to resolve activity for intent=$intent") + MainActivity.notifyError("failed to resolve activity for intent=$intent") onDenied() } }