improved error reporting when requesting directory permission
This commit is contained in:
parent
72ecbf3cb5
commit
f385f16209
9 changed files with 42 additions and 33 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue