Embed a custom plugin

Updated

Develop a custom plugin, such as a countdown plugin or a dice, and embed the plugin in the flexible classroom.

In online classroom applications, some customization is often necessary to meet the needs of a particular use case. Agora provides Widgets to help users develop plugins according to their specific needs and embed them into Flexible Classroom.

Android

Implement a custom plugin

This section uses the cloud disk plug-in as an example to introduce the basic steps of implementing a custom plug-in through Widget, and embedding the plug-in in Flexible Classroom. The complete code can be viewed in the CloudClass-Android repository.

  1. Define the class

    class FCRCloudDiskWidget : AgoraBaseWidget() {
        override val tag = "AgoraEduNetworkDiskWidget"
    
        private var cloudDiskContent: AgoraEduCloudDiskWidgetContent? = null
    
        // Initialize Widget
        // Add your custom UI layout to the container here
        override fun init(container: ViewGroup) {
            super.init(container)
            container.post {
                widgetInfo?.let {
                    // Instantiate content and add CloudDisk view to container
                    cloudDiskContent = AgoraEduCloudDiskWidgetContent(container, it)
                }
            }
        }
    
        // Release used resources
        override fun release() {
            cloudDiskContent?.dispose()
            super.release()
        }
    
        // The content class is only for encapsulation of the code.
        // Functional details are placed in the content.
        private inner class AgoraEduCloudDiskWidgetContent(val container: ViewGroup, val widgetInfo: AgoraWidgetInfo) {
            ......
        }
    }

    The complete code can be viewed in the CloudClass-Android repository.

  2. Configure the plugin in to register it with the Agora Classroom SDK

    private fun configPublicCourseware(launchConfig: AgoraEduLaunchConfig) {
        val courseware0 = CoursewareUtil.transfer(DefaultPublicCoursewareJson.data0)
        val courseware1 = CoursewareUtil.transfer(DefaultPublicCoursewareJson.data1)
        val publicCoursewares = ArrayList<AgoraEduCourseware>(2)
        publicCoursewares.add(courseware0)
        publicCoursewares.add(courseware1)
    
        // custom data for map structure
        val cloudDiskExtra = mutableMapOf<String, Any>()
        cloudDiskExtra[publicResourceKey] = publicCoursewares
        cloudDiskExtra[configKey] = Pair(launchConfig.appId, launchConfig.userUuid)
        val widgetConfigs = mutableListOf<AgoraWidgetConfig>()
        widgetConfigs.add(
            // Instantiate widgetConfig and add it to widgetConfig collection
            AgoraWidgetConfig(widgetClass = FCRCloudDiskWidget::class.java, widgetId = AgoraWidgetDefaultId.AgoraCloudDisk.id,
                extraInfo = cloudDiskExtra)
        )
        // Copy widgetConfigs into the SDK's startup parameters.
        // After calling launch, Smart Class will pass widgetConfigs to internal to complete the registration.
        launchConfig.widgetConfigs = widgetConfigs
    }
  3. Instantiate and initialize the widget

    // Add an AgoraWidgetActiveObserver through widgetContext.addWidgetActiveObserver
    // to monitor the activation and deregistration of the Widget
    private val widgetActiveObserver = object : AgoraWidgetActiveObserver {
        override fun onWidgetActive(widgetId: String) {
            createWidget(widgetId)
        }
    
        override fun onWidgetInActive(widgetId: String) {
            destroyWidget(widgetId)
        }
    }
    
    private fun createWidget(widgetId: String) {
        if (teachAidWidgets.contains(widgetId)) {
            AgoraLog?.w("$tag->'$widgetId' is already created, can not repeat create!")
            return
        }
        AgoraLog?.w("$tag->create teachAid that of '$widgetId'")
        // Here, the previously registered widgetConfigs are searched by widgetId
        val widgetConfig = eduContext?.widgetContext()?.getWidgetConfig(widgetId)
        widgetConfig?.let { config ->
    
            // Take widgetClass from widgetConfig and instantiate a Widget object instance through reflection
            val widget = eduContext?.widgetContext()?.create(config)
            widget?.let {
                AgoraLog?.w("$tag->successfully created '$widgetId'")
                when (widgetId) {
                    CountDown.id -> {
                        (it as? AgoraCountDownWidget)?.getWidgetMsgObserver()?.let { observer ->
                            eduContext?.widgetContext()?.addWidgetMessageObserver(observer, widgetId)
                        }
                    }
                    Selector.id -> {
                        (it as? AgoraIClickerWidget)?.getWidgetMsgObserver()?.let { observer ->
                            eduContext?.widgetContext()?.addWidgetMessageObserver(observer, widgetId)
                        }
                    }
                    Polling.id -> {
                        (it as? AgoraVoteWidget)?.getWidgetMsgObserver()?.let { observer ->
                            eduContext?.widgetContext()?.addWidgetMessageObserver(observer, widgetId)
                        }
                    }
                }
                // record widget
                teachAidWidgets[widgetId] = widget
                // Create widgetContainer and bind to root Container
                val widgetContainer = managerWidgetsContainer(allWidgetsContainer = binding.root, widgetId = widgetId)
                AgoraLog?.i("$tag->successfully created '$widgetId' container")
                widgetContainer?.let { group ->
                    AgoraLog?.w("$tag->initialize '$widgetId'")
                    // Initialize the Widget
                    widget.init(group)
                }
            }
        }
    }
    
    private fun destroyWidget(widgetId: String) {
        // remove from map
        val widget = teachAidWidgets.remove(widgetId)
        // remove UIDataProviderListener
        when (widgetId) {
            CountDown.id -> {
                (widget as? AgoraTeachAidCountDownWidget)?.getWidgetMsgObserver()?.let { observer ->
                    eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId)
                }
            }
            Selector.id -> {
                (widget as? AgoraTeachAidIClickerWidget)?.getWidgetMsgObserver()?.let { observer ->
                    eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId)
                }
            }
            Polling.id -> {
                (widget as? AgoraTeachAidVoteWidget)?.getWidgetMsgObserver()?.let { observer ->
                    eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId)
                }
            }
            AgoraCloudDisk.id -> {
                (widget as? AgoraTeachAidVoteWidget)?.getWidgetMsgObserver()?.let { observer ->
                    eduContext?.widgetContext()?.removeWidgetMessageObserver(observer, widgetId)
                }
            }
        }
        widget?.let {
            handler.post { it.release() }
            it.container?.let { group ->
                managerWidgetsContainer(binding.root, widgetId, group)
            }
        }
    }

API Reference

AgoraBaseWidget class

// Widget base class
abstract class AgoraBaseWidget {
    // The parent layout of the current Widget
    var container: ViewGroup? = null

    // Information about the current widget
    var widgetInfo: AgoraWidgetInfo? = null

    // Initialize the current widget
    // @param container The parent layout of the current Widget
    open fun init(container: ViewGroup) {
        this.container = container
    }

    // position and size ratio information of the current Widget to the remote
    // @param frame The position and size ratio information of the current Widget
    // @param contextCallback callback listener for synchronous operations
    protected fun updateSyncFrame(frame: AgoraWidgetFrame, contextCallback: EduContextCallback<Unit>? = null) {
        ......
    }

    // @param properties Map of custom room properties in the Widget to be updated.
    // Need to pass the full path: "a.b.c.d": value
    // @param cause custom update reason
    // @param contextCallback callback listener for update operations

    protected fun updateRoomProperties(
            properties: MutableMap<String, Any>,
            cause: MutableMap<String, Any>,
            contextCallback: EduContextCallback<Unit>? = null
    ) {
        ......
    }

    // @param keys The set of key values ​​for custom user properties in the widget that needs to be deleted.
    // Need to pass the full path: a.b.c.d
    // @param cause custom delete reason
    // @param contextCallback callback listener for delete operation
    protected fun deleteRoomProperties(
            keys: MutableList<String>,
            cause: MutableMap<String, Any>,
            contextCallback: EduContextCallback<Unit>? = null
    ) {
        ......
    }

    // @param properties Map of custom user properties within the Widget to be updated. Need to pass the full path: "a.b.c.d": value
    // @param cause custom update reason
    // @param contextCallback callback listener for update operations
    protected fun updateUserProperties(
        properties: MutableMap<String, Any>,
        cause: MutableMap<String, Any>,
        contextCallback: EduContextCallback<Unit>? = null
    ) {
        ......
    }

    // @param keys The set of key values ​​for custom user properties in the widget that needs to be deleted. Need to pass the full path: a.b.c.d
    // @param cause custom delete reason
    // @param contextCallback callback listener for delete operation
    protected fun deleteUserProperties(
        keys: MutableList<String>,
        cause: MutableMap<String, Any>,
        contextCallback: EduContextCallback<Unit>? = null
    ) {
        ......
    }

    // Send messages from inside the Widget to the outside
    protected fun sendMessage(message: String) {
        ......
    }

    // Widget's position and size changed
    // @params frame Widget position and size scale information
    open fun onSyncFrameUpdated(frame: AgoraWidgetFrame) {
    }

    // Receive messages from outside the widget. The message comes from sendMessageToWidget in widgetContext
    open fun onMessageReceived(message: String) {
    }

    // Callback for the update of local user information
    open fun onLocalUserInfoUpdated(info: AgoraWidgetUserInfo) {
        widgetInfo?.let {
            ......
        }
    }

    // Callback when room information is updated
    open fun onRoomInfoUpdated(info: AgoraWidgetRoomInfo) {
        widgetInfo?.let {
            ......
        }
    }

    // Callback for when the custom room property in the widget is updated
    open fun onWidgetRoomPropertiesUpdated(properties: MutableMap<String, Any>, cause: MutableMap<String, Any>?, keys: MutableList<String>) {
        ......
    }

    // Callback when custom room property in Widget is deleted
    open fun onWidgetRoomPropertiesDeleted(properties: MutableMap<String, Any>, cause: MutableMap<String, Any>?, keys: MutableList<String>) {
        ......
    }

    // Callback for when custom local user properties in Widget are updated
    open fun onWidgetUserPropertiesUpdated(properties: MutableMap<String, Any>,
        cause: MutableMap<String, Any>?,
        keys: MutableList<String>) {
        ......
    }

    // The callback for the deletion of the custom local user attribute in the Widget
    open fun onWidgetUserPropertiesDeleted(properties: MutableMap<String, Any>,
        cause: MutableMap<String, Any>?,
        keys: MutableList<String>) {
        ......
    }

    // Release resources
    open fun release() {
        ......
    }
}

AgoraWidgetContext interface

// WidgetContext capability interface
interface AgoraWidgetContext {
    // Create a Widget object instance
    // @param config The configuration information of this Widget object
    // @return Widget instance, if it is empty, it means the creation failed
    fun create(config: AgoraWidgetConfig): AgoraBaseWidget?

    // Get the current position and size ratio information of the Widget
    // @param widgetId Widget's unique identifier
    // @return widget's current position and scale information. The benchmark of the ratio needs to be unified by the developers themselves.
    fun getWidgetSyncFrame(widgetId: String): AgoraWidgetFrame?

    // Get the configuration information of all registered widgets
    fun getWidgetConfigs(): MutableList<AgoraWidgetConfig>

    // Get the configuration information of a registered widget
    // @param widgetId Widget's unique identifier
    fun getWidgetConfig(widgetId: String): AgoraWidgetConfig?

    // Activate a widget.
    // After the operation is successful, you will receive a callback from AgoraWidgetActiveObserver.onWidgetActive
    // @param widgetId Widget's unique identifier
    // @param ownerUserUuid The userUuid of the user who owns the currently activated Widget
    // @param roomProperties initialized room properties
    // @param callback The callback listener for the activation operation
    fun setWidgetActive(widgetId: String, ownerUserUuid: String? = null,
                        roomProperties: Map<String, Any>? = null,
                        callback: EduContextCallback<Unit>? = null)

    // Log out the specified Widget
    // @param widgetId Widget's unique identifier
    // @param isRemove Whether to completely delete all the information of this Widget in the current classroom:
    // - true: delete completely. All information under roomProperties.widgets.'widgetId' and userProperties.widgets.'widgetId' will be deleted
    // - false: Only set roomProperties.widgets.'widgetId'.state to '0', that is, set the current Widget to inactive state.
    // No matter what value is passed, you will receive the AgoraWidgetActiveObserver.onWidgetInActive callback.
    // @param callback callback listener for logout operation
    fun setWidgetInActive(widgetId: String, isRemove: Boolean = false, callback: EduContextCallback<Unit>? = null)

    // Get the activation state of a widget
    // @param widgetId Widget's unique identifier
    fun getWidgetActive(widgetId: String): Boolean

    // Get the activation status of all registered widgets
    // @param widgetId Widget's unique identifier
    fun getAllWidgetActive(): Map<String, Boolean>

    // Add an AgoraWidgetActiveObserver listener to monitor whether the Widget is active
    fun addWidgetActiveObserver(observer: AgoraWidgetActiveObserver, widgetId: String)

    // Remove an AgoraWidgetActiveObserver listener
    fun removeWidgetActiveObserver(observer: AgoraWidgetActiveObserver, widgetId: String)

    // Add an AgoraWidgetMessageObserver listener to listen for all messages sent by this Widget
    fun addWidgetMessageObserver(observer: AgoraWidgetMessageObserver, widgetId: String)

    // Remove an AgoraWidgetMessageObserver listener
    fun removeWidgetMessageObserver(observer: AgoraWidgetMessageObserver, widgetId: String)

    // Send a message to a widget
    fun sendMessageToWidget(msg: String, widgetId: String)
}

Type definition

// Widget configuration class
data class AgoraWidgetConfig(
    // The custom Widget class corresponding to this WidgetId
    var widgetClass: Class<out AgoraBaseWidget>,
    // Unique identifier for the current Widget
    var widgetId: String,
    // The icon in the unselected (default or inactive) state
    val image: Int? = null,
    // The icon in the selected or activated state
    val selectedImage: Int? = null,
    // Custom data, which is passed to AgoraWidgetInfo.extraInfo after Widget is instantiated
    var extraInfo: Any? = null
)

// Widget information class
data class AgoraWidgetInfo(
    var roomInfo: AgoraWidgetRoomInfo,
    var localUserInfo: AgoraWidgetUserInfo,
    // Unique identifier for the current Widget
    val widgetId: String,
    // Custom data passed in externally
    val extraInfo: Any?,
    // custom room properties in the current widget
    var roomProperties: MutableMap<String, Any>?,
    // Custom local user properties in the current Widget
    var localUserProperties: MutableMap<String, Any>?
)

// Widget's position and size ratio information class
data class AgoraWidgetFrame(
    val x: Float? = null,
    val y: Float? = null,
    val width: Float? = null,
    val height: Float? = null
)

// User information class in Widget
data class AgoraWidgetUserInfo(
        val userUuid: String,
        val userName: String,
        val userRole: Int
)

// Room information class in Widget
data class AgoraWidgetRoomInfo(
        val roomUuid: String,
        val roomName: String,
        val roomType: Int
)