Skip to content

Widget Extensions

SAP BTP SDK for Android version 4.0 now supports a simplified means of developing app widgets.

The UI component of app widget development needs to be handled in the client code. SAP BTP SDK for Android provides the AppExtensionService service to facilitate building a read-only okHttpClient that causes the client code to call APIs that access the resources at the server side.

When the client code tries to access the server resources using API calls, the user's secure store must be opened so that the user credentials can be retrieved to construct the okHttpClient. But this is not ideal for app widgets, which usually require that information be retrieved from the server without the host app running.

To address this constraint, the following support has been added:

  • OAuth applications can now exchange a read-only OAuth token for app widget development.
  • A new AppExtensionService service exchanges the read-only token automatically when the user signs in.
  • The new buildReadonlyOkHttpClient API facilitates the building of the okHttpClient.

Note: To create a widget extension for an app built using the SAP BTP SDK for Android, you must follow the standard procedure for App widgets.

AppExtensionService

The important AppExtensionService functions are:

class AppExtensionService @JvmOverloads constructor(
    private var appConfig: AppConfig? = null,
    private val clientFilter: ((List<AbstractOAuthClient>) -> OAuthClient)? = null,
    private val serviceReadyListener: ((ready: Boolean) -> Unit)? = null
) : MobileService() {
    ...
    fun getAppConfig(): AppConfig? = ...
    suspend fun updateAppConfig(appConfig: AppConfig, replace: Boolean = false) { ... }
    fun buildReadonlyOkHttpClient(): OkHttpClient? { ... }
}
  • appConfig (optional): If the flows component is used for onboarding, the client code does not need to pass this in when initializing the service. Otherwise, the client code needs this argument in the constructor, or can call the updateAppConfig API later.
  • clientFilter: Selects the OAuth clients in AppConfig to be used for exchanging the read-only token. If not provided, the first client in the list will be used. Using the flows component, the client code can also specify which OAuth client to use for authentication for the host app. AppExtensionService can use the same filter to use the same OAuth client to exchange the read-only token. It can also have its own filter to use a different OAuth client than the host app.
  • serviceReadyListener: Notifies the client code with the status change of this service. Because the access token and the refresh token of the read-only OAuth token will be expired (as well as in the host app), the ready status of this service may change during the lifecycle of the app. In this case, the buildReadonlyOkHttpClient API can return null, so you need to ensure that the client code handles such cases carefully.

To initialize this service, you can use the following code in onCreate of your Application:

    val services = mutableListOf<MobileService>()
    services.add(AppExtensionService(
        serviceReadyListener = { ready ->
            if (ready) {
                logger.debug("AppExtensionService ready.")
                sendBroadcast(Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE))
            }
        }
    ))
    SDKInitializer.start(
        this,
        services = services.toTypedArray()
    )

To use the read-only token to make an API call:

override fun onDataSetChanged() {
    SDKInitializer.getService(AppExtensionService::class)?.also { service ->
        service.buildReadonlyOkHttpClient()?.also { okHttpClient ->
            service.getAppConfig()?.also { appConfig ->
                runBlocking {
                    val rows =
                        ODataService(appConfig = appConfig, okHttpClient = okHttpClient).read(0,3)
                    customers.clear()
                    customers.addAll(rows)
                }
            }
        } ?: logger.debug("AppExtensionService not ready yet.")
    } ?: logger.debug("No AppExtensionService initialized with SDKInitializer.")
}

Integration with Flows

Suppose you are developing an app widget that displays a list of customers. Upon clicking one of the customers in the widget, we want to navigate to the customer detail screen in the host app. If the flows component is used for onboarding, we will handle the following two cases:

  • The host app is not started when clicking the customer.
  • The host app is running in the background and the passcode is needed before navigating to the customer detail screen.

Both cases will need the restore flow to run before navigating to the customer detail screen, and the sign-in screen should also be brought up explicitly within the customer click event handler to prevent the 'timeout unlock' flow from executing automatically.

The new startRestoreFlowExplicitly API handles these cases by starting the restore flow explicitly and disabling the 'timeout unlock' temporarily:

fun startRestoreFlowExplicitly(
    activity: Activity,
    flowContext: FlowContext,
    flowActivityResultCallback: FlowActivityResultCallback
) { ... }

The client code can do the following upon clicking the customer in the widget:

    //Customer detail activity onResume
    override fun onResume() {
        super.onResume()
        val customerId = intent.getStringExtra(EXTRA_CUSTOMER_ID)
        logger.debug("Customer id: $customerId")
        val flowContext = FlowContext(
            appConfig = AppConfig.Builder().applicationId("app_id").build(),
            multipleUserMode = false,
            flowStateListener = MyFlowStateListener(application = application),
            flowActionHandler = MyFlowActionHandler(),
            flowOptions = FlowOptions(
                appTheme = R.style.AppTheme,
                activationOption = ActivationOption.QR_ONLY,
                excludeEula = false
            )
        )
        Flow.startRestoreFlowExplicitly(this, flowContext) { _, resultCode, _ ->
            if (resultCode == Activity.RESULT_OK) {
                binding.customerId.text = customerId
                customerId?.also { queryCustomer(it) }
            } else {
                finish()
            }
        }
    }

Last update: December 9, 2021