improved error reporting when requesting directory permission

This commit is contained in:
Thibault Deckers 2021-11-08 14:35:45 +09:00
parent 72ecbf3cb5
commit f385f16209
9 changed files with 42 additions and 33 deletions

View file

@ -17,7 +17,6 @@ import deckers.thibault.aves.channel.calls.MediaStoreHandler
import deckers.thibault.aves.channel.calls.MetadataFetchHandler import deckers.thibault.aves.channel.calls.MetadataFetchHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler 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.FlutterUtils
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
@ -155,12 +154,11 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
private inner class ServiceHandler(looper: Looper) : Handler(looper) { private inner class ServiceHandler(looper: Looper) : Handler(looper) {
override fun handleMessage(msg: Message) { override fun handleMessage(msg: Message) {
val context = this@AnalysisService
val data = msg.data val data = msg.data
when (data.getString(KEY_COMMAND)) { when (data.getString(KEY_COMMAND)) {
COMMAND_START -> { COMMAND_START -> {
runBlocking { runBlocking {
context.runOnUiThread { FlutterUtils.runOnUiThread {
val contentIds = data.get(KEY_CONTENT_IDS)?.takeIf { it is IntArray }?.let { (it as IntArray).toList() } val contentIds = data.get(KEY_CONTENT_IDS)?.takeIf { it is IntArray }?.let { (it as IntArray).toList() }
backgroundChannel?.invokeMethod( backgroundChannel?.invokeMethod(
"start", hashMapOf( "start", hashMapOf(
@ -174,7 +172,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
COMMAND_STOP -> { COMMAND_STOP -> {
// unconditionally stop the service // unconditionally stop the service
runBlocking { runBlocking {
context.runOnUiThread { FlutterUtils.runOnUiThread {
backgroundChannel?.invokeMethod("stop", null) backgroundChannel?.invokeMethod("stop", null)
} }
} }

View file

@ -311,7 +311,10 @@ class MainActivity : FlutterActivity() {
var errorStreamHandler: ErrorStreamHandler? = null 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)
}
} }
} }

View file

@ -12,7 +12,6 @@ import android.text.format.DateFormat
import android.util.Log import android.util.Log
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.ContextUtils.resourceUri 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.FlutterUtils
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
@ -80,7 +79,7 @@ class SearchSuggestionsProvider : MethodChannel.MethodCallHandler, ContentProvid
return suspendCoroutine { cont -> return suspendCoroutine { cont ->
GlobalScope.launch { GlobalScope.launch {
context.runOnUiThread { FlutterUtils.runOnUiThread {
backgroundChannel.invokeMethod("getSuggestions", hashMapOf( backgroundChannel.invokeMethod("getSuggestions", hashMapOf(
"query" to query, "query" to query,
"locale" to Locale.getDefault().toString(), "locale" to Locale.getDefault().toString(),

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.streams package deckers.thibault.aves.channel.streams
import deckers.thibault.aves.utils.FlutterUtils
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink import io.flutter.plugin.common.EventChannel.EventSink
@ -15,9 +16,11 @@ class ErrorStreamHandler : EventChannel.StreamHandler {
override fun onCancel(arguments: Any?) {} override fun onCancel(arguments: Any?) {}
fun notifyError(error: String) { suspend fun notifyError(error: String) {
FlutterUtils.runOnUiThread {
eventSink?.success(error) eventSink?.success(error)
} }
}
companion object { companion object {
const val CHANNEL = "deckers.thibault/aves/error" const val CHANNEL = "deckers.thibault/aves/error"

View file

@ -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? val path = args["path"] as String?
if (path == null) { if (path == null) {
error("requestDirectoryAccess-args", "failed because of missing arguments", null) error("requestDirectoryAccess-args", "failed because of missing arguments", null)

View file

@ -323,6 +323,7 @@ class MediaStoreImageProvider : ImageProvider() {
// with a path, and retrieve its content URI, but: // 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 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 // - 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?) // - 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 // - 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 // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage

View file

@ -5,10 +5,6 @@ import android.app.Service
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Handler
import android.os.Looper
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object ContextUtils { object ContextUtils {
fun Context.resourceUri(resourceId: Int): Uri = with(resources) { fun Context.resourceUri(resourceId: Int): Uri = with(resources) {
@ -20,19 +16,6 @@ object ContextUtils {
.build() .build()
} }
suspend fun Context.runOnUiThread(r: Runnable) {
if (Looper.myLooper() != mainLooper) {
suspendCoroutine<Boolean> { cont ->
Handler(mainLooper).post {
r.run()
cont.resume(true)
}
}
} else {
r.run()
}
}
fun Context.isMyServiceRunning(serviceClass: Class<out Service>): Boolean { fun Context.isMyServiceRunning(serviceClass: Class<out Service>): Boolean {
val am = this.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? val am = this.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager?
am ?: return false am ?: return false

View file

@ -1,13 +1,16 @@
package deckers.thibault.aves.utils package deckers.thibault.aves.utils
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import deckers.thibault.aves.utils.ContextUtils.runOnUiThread
import io.flutter.FlutterInjector import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.embedding.engine.loader.FlutterLoader import io.flutter.embedding.engine.loader.FlutterLoader
import io.flutter.view.FlutterCallbackInformation import io.flutter.view.FlutterCallbackInformation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object FlutterUtils { object FlutterUtils {
private val LOG_TAG = LogUtils.createTag<FlutterUtils>() private val LOG_TAG = LogUtils.createTag<FlutterUtils>()
@ -20,7 +23,7 @@ object FlutterUtils {
} }
lateinit var flutterLoader: FlutterLoader lateinit var flutterLoader: FlutterLoader
context.runOnUiThread { FlutterUtils.runOnUiThread {
// initialization must happen on the main thread // initialization must happen on the main thread
flutterLoader = FlutterInjector.instance().flutterLoader().apply { flutterLoader = FlutterInjector.instance().flutterLoader().apply {
startInitialization(context) startInitialization(context)
@ -39,11 +42,25 @@ object FlutterUtils {
flutterLoader.findAppBundlePath(), flutterLoader.findAppBundlePath(),
callbackInfo callbackInfo
) )
context.runOnUiThread { runOnUiThread {
val engine = FlutterEngine(context).apply { val engine = FlutterEngine(context).apply {
dartExecutor.executeDartCallback(args) dartExecutor.executeDartCallback(args)
} }
engineSetter(engine) engineSetter(engine)
} }
} }
suspend fun runOnUiThread(r: Runnable) {
val mainLooper = Looper.getMainLooper()
if (Looper.myLooper() != mainLooper) {
suspendCoroutine<Boolean> { cont ->
Handler(mainLooper).post {
r.run()
cont.resume(true)
}
}
} else {
r.run()
}
}
} }

View file

@ -31,13 +31,18 @@ object PermissionManager {
) )
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @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") Log.i(LOG_TAG, "request user to select and grant access permission to path=$path")
var intent: Intent? = null var intent: Intent? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val sm = activity.getSystemService(Context.STORAGE_SERVICE) as? StorageManager 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 // fallback to basic open document tree intent
@ -49,7 +54,7 @@ object PermissionManager {
MainActivity.pendingStorageAccessResultHandlers[MainActivity.DOCUMENT_TREE_ACCESS_REQUEST] = PendingStorageAccessResultHandler(path, onGranted, onDenied) MainActivity.pendingStorageAccessResultHandlers[MainActivity.DOCUMENT_TREE_ACCESS_REQUEST] = PendingStorageAccessResultHandler(path, onGranted, onDenied)
activity.startActivityForResult(intent, MainActivity.DOCUMENT_TREE_ACCESS_REQUEST) activity.startActivityForResult(intent, MainActivity.DOCUMENT_TREE_ACCESS_REQUEST)
} else { } else {
Log.e(LOG_TAG, "failed to resolve activity for intent=$intent") MainActivity.notifyError("failed to resolve activity for intent=$intent")
onDenied() onDenied()
} }
} }