package com.pycampers.plugin_scaffold import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry.Registrar import android.os.AsyncTask import android.os.Handler import android.util.Log import io.flutter.app.FlutterActivity import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.EventChannel.EventSink import java.io.PrintWriter import java.io.StringWriter import java.lang.reflect.Method const val TAG = "PluginScaffold" const val ON_LISTEN = "OnListen" const val ON_CANCEL = "OnCancel" const val ON_SUCCESS = "onSuccess" const val ON_ERROR = "onError" const val END_OF_STREAM = "endOfStream" private val methodSignature = listOf(MethodCall::class.java, Result::class.java) private val onListenSignature = listOf(Int::class.java, Object::class.java, EventSink::class.java) typealias OnError = (errorCode: String, errorMessage: String?, errorDetails: Any?) -> Unit typealias OnSuccess = (result: Any?) -> Unit typealias AnyFn = () -> Any? typealias UnitFn = () -> Unit typealias MethodMap = MutableMap /** * Create a plugin with the provided [channelName]. * * The methods of [pluginObj] having parameters - ([MethodCall], [Result]), * are automatically exposed through the returned [MethodChannel]: * * The [messenger] can be passed in various ways. * - If you're inside a subclass of [FlutterActivity], pass [FlutterActivity.getFlutterView] (generally regular flutter apps) * - If you have access to a [Registrar] object, pass [Registrar.messenger] (generally flutter plugin projects) * * If [runOnMainThread] is set to `true`, * the methods in [pluginObj] will be invoked from the main thread (using [Handler.post]). * Otherwise, they will be invoked from an [AsyncTask]. */ fun createPluginScaffold( messenger: BinaryMessenger, channelName: String, pluginObj: Any = Any(), runOnMainThread: Boolean = false ): MethodChannel { val methods = buildMethodMap(pluginObj) val (onListenMethods, onCancelMethods) = buildStreamMethodMap(pluginObj) val channel = MethodChannel(messenger, channelName) val wrapper = createMethodWrapper(runOnMainThread) channel.setMethodCallHandler { call, result -> val name = call.method val args = call.arguments val mainResult = MainThreadResult(result) // // Try to find the method in [methods], [onListenMethods] and [onCancelMethods], // and invoke it using [wrapFunCall]. // // If not found, invoke [Result.notImplemented] // methods[name]?.let { Log.d(TAG, "invoke { channel: $channelName, method: $name(), args: $args }") wrapper(mainResult) { it.invoke(pluginObj, call, mainResult) } return@setMethodCallHandler } onListenMethods[name]?.let { val streamName = getStreamName(name)!! val (hashCode: Any?, streamArgs: Any?) = args as List<*> val prefix = "$streamName/$hashCode" val sink = MainThreadEventSink(channel, prefix) Log.d( TAG, "activate stream { channel: $channelName, stream: $streamName, hashCode: $hashCode, args: $streamArgs }" ) wrapper(mainResult) { it.invoke(pluginObj, hashCode, streamArgs, sink) } return@setMethodCallHandler } onCancelMethods[name]?.let { val streamName = getStreamName(name)!! val (hashCode: Any?, streamArgs: Any?) = args as List<*> Log.d( TAG, "de-activate stream { channel: $channelName, stream: $streamName, hashCode: $hashCode, args: $streamArgs }" ) wrapper(mainResult) { it.invoke(pluginObj, hashCode, streamArgs) } return@setMethodCallHandler } mainResult.notImplemented() } return channel } fun createMethodWrapper(runOnMainThread: Boolean): (Result, UnitFn) -> Unit { if (runOnMainThread) { return { result, fn -> handler.post { catchErrors(result, fn) } } } else { return { result, fn -> DoAsync { catchErrors(result, fn) } } } } /** * Try to send the value returned by [fn] using [onSuccess]. * * It is advisable to wrap any native code inside [fn], * because this will automatically catch and send to dart exceptions using * using [sendThrowable] and [onError] if required. */ fun trySend(onSuccess: OnSuccess, onError: OnError, fn: AnyFn? = null) { val value: Any? try { value = fn?.invoke() onSuccess(if (value is Unit) null else value) } catch (e: Throwable) { sendThrowable(onError, e) } } fun trySend(result: Result, fn: AnyFn? = null) { trySend(result::success, result::error, fn) } fun trySend(events: EventSink, fn: AnyFn? = null) { trySend(events::success, events::error, fn) } /** * Run [fn]. * Automatically send exceptions using error using [sendThrowable] if required. * * This differs from [trySend], * in that it won't invoke [Result.success] using the return value of [fn]. */ fun catchErrors(onError: OnError, fn: UnitFn) { try { fn() } catch (e: Throwable) { sendThrowable(onError, e) } } fun catchErrors(result: Result, fn: UnitFn) { catchErrors(result::error, fn) } fun catchErrors(events: EventSink, fn: UnitFn) { catchErrors(events::error, fn) } /** * Serialize the [throwable] and send it using [onError]. */ fun sendThrowable(onError: OnError, throwable: Throwable) { val e = throwable.cause ?: throwable onError( e.javaClass.canonicalName ?: "null", e.message, serializeStackTrace(e) ) } fun sendThrowable(result: Result, throwable: Throwable) { sendThrowable(result::error, throwable) } fun sendThrowable(events: EventSink, throwable: Throwable) { sendThrowable(events::error, throwable) } /** * Serialize the stacktrace contained in [throwable] to a [String]. */ fun serializeStackTrace(throwable: Throwable): String { val sw = StringWriter() val pw = PrintWriter(sw) throwable.printStackTrace(pw) return sw.toString() } fun buildMethodMap(pluginObj: Any): MethodMap { val map: MethodMap = mutableMapOf() for (method in pluginObj::class.java.methods) { val paramList = method.parameterTypes.toList() if (paramList != methodSignature) continue map[method.name] = method } return map } fun buildStreamMethodMap(pluginObj: Any): Pair { val onListenMethods: MethodMap = mutableMapOf() val onCancelMethods: MethodMap = mutableMapOf() val cls = pluginObj::class.java for (listenMethod in cls.methods) { val paramList = listenMethod.parameterTypes.toList() if (paramList != onListenSignature) continue val onListenName = listenMethod.name val streamName = getStreamName(onListenName) ?: continue val onCancelName = streamName + ON_CANCEL val cancelMethod: Method try { cancelMethod = cls.getMethod(onCancelName, Int::class.java, Object::class.java) } catch (e: NoSuchMethodException) { Log.w( TAG, "Found \"$onListenName()\" in \"$cls\", but accompanying method \"$onCancelName()\" was not found!" ) continue } onListenMethods[onListenName] = listenMethod onCancelMethods[onCancelName] = cancelMethod } return Pair(onListenMethods, onCancelMethods) } fun getStreamName(methodName: String): String? { val name = when { methodName.endsWith(ON_LISTEN) -> methodName.substring( 0, methodName.length - ON_LISTEN.length ) methodName.endsWith(ON_CANCEL) -> methodName.substring( 0, methodName.length - ON_CANCEL.length ) else -> null } if (name != null && name.isNotEmpty()) { return name } return null } class DoAsync(val fn: () -> Unit) : AsyncTask() { init { execute() } override fun doInBackground(vararg params: Void?): Void? { fn() return null } } /** PluginScaffoldPlugin */ public class PluginScaffoldPlugin: FlutterPlugin, MethodCallHandler { override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { val channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "plugin_scaffold") channel.setMethodCallHandler(PluginScaffoldPlugin()); } // This static function is optional and equivalent to onAttachedToEngine. It supports the old // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting // plugin registration via this function while apps migrate to use the new Android APIs // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. // // It is encouraged to share logic between onAttachedToEngine and registerWith to keep // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called // depending on the user's project. onAttachedToEngine or registerWith must both be defined // in the same class. companion object { @JvmStatic fun registerWith(registrar: Registrar) { val channel = MethodChannel(registrar.messenger(), "plugin_scaffold") channel.setMethodCallHandler(PluginScaffoldPlugin()) } } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "getPlatformVersion") { result.success("Android ${android.os.Build.VERSION.RELEASE}") } else { result.notImplemented() } } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { } }