Build and consume a Runtime-Enabled SDK

1
Key concepts
2
Set up your development environment
3
Build an RE SDK
4
Consume the RE SDK
5
Testing, and building for distribution

Consume the runtime-enabled SDK

This section describes how clients can interact with the declared runtime-enabled (RE) SDK APIs.

In the case of this guide, we refer to your existing SDK module (or runtime-aware SDK) as the client.

If you want to the runtime-enabled SDK directly into your app, the app module is the client.

Load the runtime-enabled SDK

The first thing you want to do on your runtime-aware SDK or client app is load the runtime-enabled SDK.

The SdkSandboxManager class assists with loading runtime-enabled SDKs, returning an IBinder class that the runtime-aware SDK can bind to the interface declared in the runtime-enabled SDK.

You need to ensure that you only load each runtime-enabled SDK once, or the SDK manager will return an exception.

The shim generation tools generate helper classes to convert the IBinder interface returned by the SdkSandboxManager back to the declared SDK API interface.

The tools use the interface annotated with @PrivacySandboxService to generate a *Factory class.

This class contains a static wrapTo* function that converts an IBinder object into an instance of your runtime-enabled SDK's interface.

Your runtime-aware SDK can communicate with the runtime-enabled SDK using this interface, and invoke the SDK APIs you declared in the previous step.

// Name of the SDK to be loaded, defined in your ASB module
private const val SDK_NAME = "com.example.sdk"

try {
    // SdkSandboxManagerCompat is used to communicate with the sandbox and load SDKs with backward compatibility.
    val sandboxManagerCompat = SdkSandboxManagerCompat.from(context)
    val sandboxedSdk = sandboxManagerCompat.loadSdk(SDK_NAME, Bundle.EMPTY)
    val mySdk = MySdkFactory.wrapToMySdk(sandboxedSdk.getInterface()!!)
} catch (e: LoadSdkCompatException) {
    Log.e(TAG, "Failed to load SDK, error code: ${e.loadSdkErrorCode}", e)
    return null
}

UI library usage

If you want to use the UI library to display ads, make sure you have added androidx.privacysandbox.ui:ui-core and androidx.privacysandbox.ui:ui-client to the dependencies in the build.gradle of your runtime-aware SDK.

Load a banner ad using SandboxedSdkView

androidx.privacysandbox.ui:ui-client introduces a new ViewGroup called SandboxedSdkView to host UI created by a runtime-enabled SDK.

setAdapter() opens a session with the runtime-enabled SDK to receive the ad view and notifications of UI changes. When the SDK opens the session, the ad is shown.

This could be integrated as follows:

class BannerAd(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
    suspend fun loadAd() {
        // mySdk is the previously loaded SDK in the SDK Runtime.
        val bannerAd = mySdk.loadAd()
        val sandboxedSdkView = SandboxedSdkView(context)
        addViewToLayout(sandboxedSdkView)

        // This renders the ad.
        sandboxedSdkView.setAdapter(bannerAd)
        return
    }
    private fun addViewToLayout(view: View) {
        view.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
        super.addView(view)
    }
}

Your runtime-aware SDK can also get notified when the session state changes for the UI presentation. To do this:

  1. Create a SessionStateChangeListener() class to handle the different scenarios:

    private class SessionStateChangeListener() : SandboxedSdkUiSessionStateChangedListener {
        override fun onStateChanged(state: SandboxedSdkUiSessionState) {
            if (state is SandboxedSdkUiSessionState.Error) {
            // Some error has occurred while opening the session. Handle
            // accordingly.
            Log.e(TAG, state.throwable.message!!);
            } else if (state is SandboxedSdkUiSessionState.Loading) {
                // The session is attempting to be opened.
            } else if (state is SandboxedSdkUiSessionState.Active) {
                // The session is open and the UI presentation was successful.
            } else if (state is SandboxedSdkUiSessionState.Idle) {
                // There is no open session.
            }
        }
    }
    
  2. Add a state change listener to the SandboxedSdkView you've instantiated earlier on. The listener is immediately called with the current state as soon as it's attached to the view.

Note the following:

  • If the runtime-aware SDK calls SandboxedSdkView methods when the session still has not finished opening, all effects will be applied after the session has finished opening.
    • Methods such as SandboxedSdkView.orderProviderUiAboveClientUi(providerUiOnTop)
  • Calling methods that add or remove a view from SandboxedSdkView (such as addView(), removeView(), removeViewAt(), etc.) is not supported, throwing an UnsupportedOperationException.
    • Only use setAdapter() to show the ad.
  • SandboxedSdkView.orderProviderUiAboveClientUi(providerUiOnTop) toggles Z ordering which affects whether MotionEvents from user interaction get sent to the runtime-enabled SDK or the runtime-aware SDK.

Start activities

To start activities owned by the runtime-enabled SDK, use the createSdkActivityLauncher extension to create a launcher in the runtime-aware SDK.

This launcher can then be passed to your runtime-enabled SDK, allowing it to initiate activities as needed.

You can use a predicate to control whether the activity will be launched or not. The predicate needs to return a true value for activities to be allowed.

val launchSdkActivityPredicate = {
    // Boolean which has to be true to launch the activities
    }
val launcher = baseActivity.createSdkActivityLauncher(launchSdkActivityPredicate)
fullscreenService.showActivity(launcher)

Within your runtime-enabled SDK, register SdkSandboxActivityHandlerCompat, and provide it to SdkActivityLauncher.LaunchSdkActivity(IBinder).

fun showActivity(activityLauncher: SdkActivityLauncher) {
    val handler = object : SdkSandboxActivityHandlerCompat {
        override fun onActivityCreated(activityHolder: ActivityHolder) {
            activityHolder.getActivity().setContentView(contentView)
        }
    }

    val token = controller.registerSdkSandboxActivityHandler(handler)
    activityLauncher.launchSdkActivity(token)
}

The ActivityHolder passed to SdkSandboxActivityHandlerCompat.onActivityCreated(ActivityHolder) implements LifecycleOwner, giving your runtime-enabled SDK access to the activity's lifecycle.

It also provides the getOnBackPressedDispatcher API, which can be used to register getOnBackPressedCallback instances to handle the back button behavior within the activity.


Step 3: Build a runtime-enabled SDK Step 5: Testing and building for distribution