crossbow-android 0.2.3

Cross-Platform build tools and toolkit for games
Documentation
@file:Suppress("UNUSED_PARAMETER")

package com.crossbow.library.plugin

import com.crossbow.library.Crossbow
import java.util.concurrent.ConcurrentHashMap
import com.crossbow.library.plugin.SignalInfo
import android.app.Activity
import com.crossbow.library.plugin.CrossbowPlugin
import android.view.View
import android.content.Intent
import javax.microedition.khronos.opengles.GL10
import java.lang.Runnable
import java.lang.IllegalArgumentException
import android.util.Log
import android.view.Surface
import com.crossbow.library.plugin.CrossbowPluginInfoProvider
import java.lang.NoClassDefFoundError
import java.lang.Class
import java.lang.reflect.Method
import com.crossbow.library.plugin.ExposedToCrossbow
import com.crossbow.library.JNIUtil
import java.util.HashMap
import javax.microedition.khronos.egl.EGLConfig

/**
 * Base class for the Crossbow Android plugins.
 *
 *
 * A Crossbow Android plugin is a regular Android library packaged as an aar archive file with the following caveats:
 *
 *
 * - The library must have a dependency on the Crossbow Android library (crossbow-lib.aar).
 * A stable version is available for each release.
 *
 *
 * - The library must include a <meta-data> tag in its manifest file setup as follow:
 * <meta-data android:name="com.crossbow.plugin.v1.[PluginName]" android:value="[plugin.init.ClassFullName]"></meta-data>
 * Where:
 * - 'PluginName' is the name of the plugin.
 * - 'plugin.init.ClassFullName' is the full name (package + class name) of the plugin class
 * extending [CrossbowPlugin].
</meta-data> */
abstract class CrossbowPlugin(
    /**
     * Provides access to the Crossbow engine.
     */
    protected val crossbow: Crossbow
) {
    private val registeredSignals = ConcurrentHashMap<String, SignalInfo?>()

    /**
     * Provides access to the underlying [Activity].
     */
    protected val activity: Activity?
        @Suppress("DEPRECATION")
        get() = crossbow.activity

    /**
     * Register the plugin with Crossbow native code.
     *
     * This method is invoked on the render thread.
     */
    open fun onRegisterPluginWithCrossbowNative() {
        registeredSignals.putAll(registerPluginWithCrossbowNative(this, pluginName, pluginSignals))
    }

    /**
     * Invoked once during the Crossbow Android initialization process after creation of the
     * [com.crossbow.library.CrossbowRenderView] view.
     *
     *
     * The plugin can return a non-null [View] layout in order to add it to the Crossbow view
     * hierarchy.
     *
     * Use shouldBeOnTop() to set whether the plugin's [View] should be added on top or behind
     * the main Crossbow view.
     *
     * @see Activity.onCreate
     * @return the plugin's view to be included; null if no views should be included.
     */
    open fun onMainCreate(pActivity: Activity): View? {
        return null
    }

    /**
     * @see Activity.onActivityResult
     */
    open fun onMainActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {}

    /**
     * @see Activity.onRequestPermissionsResult
     */
    open fun onMainRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray?
    ) {
    }

    /**
     * @see Activity.onPause
     */
    open fun onMainPause() {}

    /**
     * @see Activity.onResume
     */
    open fun onMainResume() {}

    /**
     * @see Activity.onDestroy
     */
    open fun onMainDestroy() {}

    /**
     * @see Activity.onBackPressed
     */
    open fun onMainBackPressed(): Boolean {
        return false
    }

    /**
     * Invoked on the render thread when the Crossbow setup is complete.
     */
    open fun onCrossbowSetupCompleted() {}

    /**
     * Invoked on the render thread when the Crossbow main loop has started.
     */
    open fun onCrossbowMainLoopStarted() {}

    /**
     * Invoked once per frame on the GL thread after the frame is drawn.
     */
    open fun onGLDrawFrame(gl: GL10?) {}

    /**
     * Called on the GL thread after the surface is created and whenever the OpenGL ES surface size
     * changes.
     */
    open fun onGLSurfaceChanged(gl: GL10?, width: Int, height: Int) {}

    /**
     * Called on the GL thread when the surface is created or recreated.
     */
    open fun onGLSurfaceCreated(gl: GL10?, config: EGLConfig?) {}

    /**
     * Invoked once per frame on the Vulkan thread after the frame is drawn.
     */
    open fun onVkDrawFrame() {}

    /**
     * Called on the Vulkan thread after the surface is created and whenever the surface size
     * changes.
     */
    open fun onVkSurfaceChanged(surface: Surface?, width: Int, height: Int) {}

    /**
     * Called on the Vulkan thread when the surface is created or recreated.
     */
    open fun onVkSurfaceCreated(surface: Surface?) {}

    /**
     * Returns the name of the plugin.
     *
     * This value must match the one listed in the plugin '<meta-data>' manifest entry.
     */
    abstract val pluginName: String

    /**
     * Returns the list of signals to be exposed to Crossbow.
     */
    open val pluginSignals: Set<SignalInfo>
        get() = emptySet<SignalInfo>()

    /**
     * Returns whether the plugin's [View] returned in onMainCreate() should be placed on
     * top of the main Crossbow view.
     *
     * Returning false causes the plugin's [View] to be placed behind, which can be useful
     * when used with transparency in order to let the Crossbow view handle inputs.
     */
    open fun shouldBeOnTop(): Boolean {
        return true
    }

    /**
     * Runs the specified action on the UI thread. If the current thread is the UI
     * thread, then the action is executed immediately. If the current thread is
     * not the UI thread, the action is posted to the event queue of the UI thread.
     *
     * @param action the action to run on the UI thread
     */
    protected fun runOnUiThread(action: Runnable?) {
        crossbow.runOnUiThread(action!!)
    }

    /**
     * Queue the specified action to be run on the render thread.
     *
     * @param action the action to run on the render thread
     */
    // protected fun runOnRenderThread(action: Runnable?) {
    // 	crossbow.runOnRenderThread(action)
    // }

    /**
     * Emit a registered Crossbow signal.
     * @param signalName Name of the signal to emit. It will be validated against the set of registered signals.
     * @param signalArgs Arguments used to populate the emitted signal. The arguments will be validated against the [SignalInfo] matching the registered signalName parameter.
     */
    protected fun emitSignal(signalName: String, vararg signalArgs: Any?) {
        try {
            // Check that the given signal is among the registered set.
            val signalInfo = registeredSignals[signalName]
                ?: throw IllegalArgumentException(
                    "Signal $signalName is not registered for this plugin."
                )
            emitSignal(crossbow, pluginName, signalInfo, *signalArgs)
        } catch (exception: IllegalArgumentException) {
            Log.w(TAG, exception.message!!)
            // if (BuildConfig.DEBUG) {
            // 	throw exception;
            // }
        }
    }

    companion object {
        private val TAG = CrossbowPlugin::class.java.simpleName

        /**
         * Register the plugin with Crossbow native code.
         *
         * This method must be invoked on the render thread.
         */
        fun registerPluginWithCrossbowNative(
            pluginObject: Any,
            pluginInfoProvider: CrossbowPluginInfoProvider
        ) {
            try {
                registerPluginWithCrossbowNative(
                    pluginObject,
                    pluginInfoProvider.pluginName,
                    pluginInfoProvider.pluginSignals
                )
            } catch (e: NoClassDefFoundError) {
                Log.e(TAG, "Error getting declared methods", e)
            }
            // Notify that registration is complete.
            pluginInfoProvider.onPluginRegistered()
        }

        private fun registerPluginWithCrossbowNative(
            pluginObject: Any,
            pluginName: String,
            pluginSignals: Set<SignalInfo?>
        ): Map<String, SignalInfo?> {
            nativeRegisterSingleton(pluginName, pluginObject)
            val clazz: Class<*> = pluginObject.javaClass
            val methods = clazz.declaredMethods
            Log.i(TAG, "Registering plugin: " + pluginName + " (" + clazz.name + ")")
            for (method in methods) {
                // Check if the method is annotated with {@link ExposedToCrossbow}.
                if (method.getAnnotation(ExposedToCrossbow::class.java) != null) {
                    val sig = JNIUtil.getJNIMethodSignature(method)
                    nativeRegisterMethod(pluginName, method.name, sig)
                    Log.i(TAG, "Registered " + pluginName + " plugin method: " + method.name)
                }
            }

            // Register the signals for this plugin.
            val registeredSignals: MutableMap<String, SignalInfo?> = HashMap()
            for (signalInfo in pluginSignals) {
                val signalName = signalInfo!!.name
                nativeRegisterSignal(pluginName, signalName, signalInfo.paramTypesNames)
                registeredSignals[signalName] = signalInfo
            }
            return registeredSignals
        }

        /**
         * Emit a Crossbow signal.
         * @param crossbow
         * @param pluginName Name of the Crossbow plugin the signal will be emitted from. The plugin must already be registered with the Crossbow engine.
         * @param signalInfo Information about the signal to emit.
         * @param signalArgs Arguments used to populate the emitted signal. The arguments will be validated against the given [SignalInfo] parameter.
         */
        fun emitSignal(
            crossbow: Crossbow?,
            pluginName: String,
            signalInfo: SignalInfo?,
            vararg signalArgs: Any?
        ) {
            try {
                requireNotNull(signalInfo) { "Signal must be non null." }

                // Validate the arguments count.
                val signalParamTypes: Array<Class<*>> = signalInfo.paramTypes
                require(signalArgs.size == signalParamTypes.size) { "Invalid arguments count. Should be " + signalParamTypes.size + "  but is " + signalArgs.size }

                // Validate the argument's types.
                for (i in signalParamTypes.indices) {
                    require(signalParamTypes[i].isInstance(signalArgs[i])) { "Invalid type for argument #" + i + ". Should be of type " + signalParamTypes[i].name }
                }

                // crossbow.runOnRenderThread(() -> nativeEmitSignal(pluginName, signalInfo.getName(), signalArgs));
                nativeEmitSignal(pluginName, signalInfo.name, arrayOf(*signalArgs))
            } catch (exception: IllegalArgumentException) {
                Log.w(TAG, exception.message!!)
                // if (BuildConfig.DEBUG) {
                // 	throw exception;
                // }
            }
        }

        /**
         * Used to setup a [CrossbowPlugin] instance.
         * @param p_name Name of the instance.
         */
        @JvmStatic
        private external fun nativeRegisterSingleton(p_name: String, p_obj: Any)

        /**
         * Used to complete registration of the [CrossbowPlugin] instance's methods.
         * @param p_sname Name of the instance
         * @param p_name Name of the method to register
         * @param p_sig Signature of the registered method
         */
        @JvmStatic
        private external fun nativeRegisterMethod(p_sname: String, p_name: String, p_sig: String)

        /**
         * Used to complete registration of the [CrossbowPlugin] instance's methods.
         * @param pluginName Name of the plugin
         * @param signalName Name of the signal to register
         * @param signalParamTypes Signal parameters types
         */
        @JvmStatic
        private external fun nativeRegisterSignal(
            pluginName: String,
            signalName: String,
            signalParamTypes: Array<String>
        )

        /**
         * Used to emit signal by [CrossbowPlugin] instance.
         * @param pluginName Name of the plugin
         * @param signalName Name of the signal to emit
         * @param signalParams Signal parameters
         */
        @JvmStatic
        private external fun nativeEmitSignal(
            pluginName: String,
            signalName: String,
            signalParams: Array<Any?>
        )
    }
}