diff --git a/app/lib/mobile/android/permission_util.dart b/app/lib/mobile/android/permission_util.dart new file mode 100644 index 00000000..690d8ff0 --- /dev/null +++ b/app/lib/mobile/android/permission_util.dart @@ -0,0 +1,18 @@ +import 'package:logging/logging.dart'; +import 'package:nc_photos/stream_extension.dart'; +import 'package:nc_photos_plugin/nc_photos_plugin.dart'; + +Future> requestPermissionsForResult( + List permissions) async { + Map? result; + final resultFuture = Permission.stream + .whereType() + .first + .then((ev) => result = ev.grantResults); + await Permission.request(permissions); + await resultFuture; + _log.info("[requestPermissionsForResult] Result: $result"); + return result!; +} + +final _log = Logger("mobile.android.permission_util"); diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt index 95203f0a..2909ab2a 100644 --- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt @@ -11,7 +11,8 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry class NcPhotosPlugin : FlutterPlugin, ActivityAware, - PluginRegistry.ActivityResultListener { + PluginRegistry.ActivityResultListener, + PluginRegistry.RequestPermissionsResultListener { companion object { const val ACTION_SHOW_IMAGE_PROCESSOR_RESULT = K.ACTION_SHOW_IMAGE_PROCESSOR_RESULT @@ -81,6 +82,19 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware, contentUriMethodChannel.setMethodCallHandler( ContentUriChannelHandler(flutterPluginBinding.applicationContext) ) + + permissionChannelHandler = + PermissionChannelHandler(flutterPluginBinding.applicationContext) + permissionChannel = EventChannel( + flutterPluginBinding.binaryMessenger, + PermissionChannelHandler.EVENT_CHANNEL + ) + permissionChannel.setStreamHandler(permissionChannelHandler) + permissionMethodChannel = MethodChannel( + flutterPluginBinding.binaryMessenger, + PermissionChannelHandler.METHOD_CHANNEL + ) + permissionMethodChannel.setMethodCallHandler(permissionChannelHandler) } override fun onDetachedFromEngine( @@ -94,30 +108,39 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware, mediaStoreMethodChannel.setMethodCallHandler(null) imageProcessorMethodChannel.setMethodCallHandler(null) contentUriMethodChannel.setMethodCallHandler(null) + permissionMethodChannel.setMethodCallHandler(null) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { mediaStoreChannelHandler.onAttachedToActivity(binding) + permissionChannelHandler.onAttachedToActivity(binding) pluginBinding = binding binding.addActivityResultListener(this) + binding.addRequestPermissionsResultListener(this) } override fun onReattachedToActivityForConfigChanges( binding: ActivityPluginBinding ) { mediaStoreChannelHandler.onReattachedToActivityForConfigChanges(binding) + permissionChannelHandler.onReattachedToActivityForConfigChanges(binding) pluginBinding = binding binding.addActivityResultListener(this) + binding.addRequestPermissionsResultListener(this) } override fun onDetachedFromActivity() { mediaStoreChannelHandler.onDetachedFromActivity() + permissionChannelHandler.onDetachedFromActivity() pluginBinding?.removeActivityResultListener(this) + pluginBinding?.removeRequestPermissionsResultListener(this) } override fun onDetachedFromActivityForConfigChanges() { mediaStoreChannelHandler.onDetachedFromActivityForConfigChanges() + permissionChannelHandler.onDetachedFromActivityForConfigChanges() pluginBinding?.removeActivityResultListener(this) + pluginBinding?.removeRequestPermissionsResultListener(this) } override fun onActivityResult( @@ -141,6 +164,27 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware, } } + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ): Boolean { + return try { + when (requestCode) { + K.PERMISSION_REQUEST_CODE -> { + permissionChannelHandler.onRequestPermissionsResult( + requestCode, permissions, grantResults + ) + } + + else -> false + } + } catch (e: Throwable) { + Log.e( + TAG, "Failed while onActivityResult, requestCode=$requestCode" + ) + false + } + } + private var pluginBinding: ActivityPluginBinding? = null private lateinit var lockChannel: MethodChannel @@ -151,7 +195,10 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware, private lateinit var mediaStoreMethodChannel: MethodChannel private lateinit var imageProcessorMethodChannel: MethodChannel private lateinit var contentUriMethodChannel: MethodChannel + private lateinit var permissionChannel: EventChannel + private lateinit var permissionMethodChannel: MethodChannel private lateinit var lockChannelHandler: LockChannelHandler private lateinit var mediaStoreChannelHandler: MediaStoreChannelHandler + private lateinit var permissionChannelHandler: PermissionChannelHandler } diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/PermissionChannelHandler.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/PermissionChannelHandler.kt new file mode 100644 index 00000000..f3dc820e --- /dev/null +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/PermissionChannelHandler.kt @@ -0,0 +1,113 @@ +package com.nkming.nc_photos.plugin + +import android.app.Activity +import android.content.Context +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry + +class PermissionChannelHandler(context: Context) : + MethodChannel.MethodCallHandler, EventChannel.StreamHandler, ActivityAware, + PluginRegistry.RequestPermissionsResultListener { + companion object { + const val EVENT_CHANNEL = "${K.LIB_ID}/permission" + const val METHOD_CHANNEL = "${K.LIB_ID}/permission_method" + + private const val TAG = "PermissionChannelHandler" + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + } + + override fun onReattachedToActivityForConfigChanges( + binding: ActivityPluginBinding + ) { + activity = binding.activity + } + + override fun onDetachedFromActivity() { + activity = null + } + + override fun onDetachedFromActivityForConfigChanges() { + activity = null + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ): Boolean { + return if (requestCode == K.PERMISSION_REQUEST_CODE) { + eventSink?.success(buildMap { + put("event", "RequestPermissionsResult") + put( + "grantResults", + permissions.zip(grantResults.toTypedArray()).toMap() + ) + }) + true + } else { + false + } + } + + override fun onListen(arguments: Any?, events: EventChannel.EventSink) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "request" -> { + try { + request(call.argument("permissions")!!, result) + } catch (e: Throwable) { + result.error("systemException", e.toString(), null) + } + } + + "hasWriteExternalStorage" -> { + try { + result.success( + PermissionUtil.hasWriteExternalStorage(context) + ) + } catch (e: Throwable) { + result.error("systemException", e.toString(), null) + } + } + + "hasReadExternalStorage" -> { + try { + result.success( + PermissionUtil.hasReadExternalStorage(context) + ) + } catch (e: Throwable) { + result.error("systemException", e.toString(), null) + } + } + + else -> result.notImplemented() + } + } + + private fun request( + permissions: List, result: MethodChannel.Result + ) { + if (activity == null) { + result.error("systemException", "Activity is not ready", null) + return + } + PermissionUtil.request(activity!!, *permissions.toTypedArray()) + result.success(null) + } + + private val context = context + private var activity: Activity? = null + private var eventSink: EventChannel.EventSink? = null +} diff --git a/plugin/lib/nc_photos_plugin.dart b/plugin/lib/nc_photos_plugin.dart index 0b893113..acb92591 100644 --- a/plugin/lib/nc_photos_plugin.dart +++ b/plugin/lib/nc_photos_plugin.dart @@ -7,3 +7,4 @@ export 'src/lock.dart'; export 'src/media_store.dart'; export 'src/native_event.dart'; export 'src/notification.dart'; +export 'src/permission.dart'; diff --git a/plugin/lib/src/permission.dart b/plugin/lib/src/permission.dart new file mode 100644 index 00000000..b8787e43 --- /dev/null +++ b/plugin/lib/src/permission.dart @@ -0,0 +1,62 @@ +// ignore_for_file: constant_identifier_names + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos_plugin/src/k.dart' as k; + +class Permission { + static const READ_EXTERNAL_STORAGE = + "android.permission.READ_EXTERNAL_STORAGE"; + static const WRITE_EXTERNAL_STORAGE = + "android.permission.WRITE_EXTERNAL_STORAGE"; + + static Future request(List permissions) => + _methodChannel.invokeMethod("request", { + "permissions": permissions, + }); + + static Future hasWriteExternalStorage() async { + return (await _methodChannel + .invokeMethod("hasWriteExternalStorage"))!; + } + + static Future hasReadExternalStorage() async { + return (await _methodChannel.invokeMethod("hasReadExternalStorage"))!; + } + + static Stream get stream => _eventStream; + + static late final _eventStream = + _eventChannel.receiveBroadcastStream().map((event) { + if (event is Map) { + switch (event["event"]) { + case _eventRequestPermissionsResult: + return PermissionRequestResult( + (event["grantResults"] as Map).cast()); + + default: + _log.shout("[_eventStream] Unknown event: ${event["event"]}"); + } + } else { + return event; + } + }); + + static const _eventChannel = EventChannel("${k.libId}/permission"); + static const _methodChannel = MethodChannel("${k.libId}/permission_method"); + + static const _eventRequestPermissionsResult = "RequestPermissionsResult"; + + static final _log = Logger("plugin.permission.Permission"); +} + +class PermissionRequestResult { + static const granted = 0; + static const denied = -1; + + const PermissionRequestResult(this.grantResults); + + final Map grantResults; +}