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

Build a Runtime-Enabled SDK

You have to complete the following steps to build a runtime-enabled SDK:

  1. Set up your project structure
  2. Prepare your project and module dependencies
  3. Add your SDK business logic
  4. Define the SDK APIs
  5. Specify an entry-point for your SDK

Set up your project structure

We recommend that your project is organized into the following modules:

  1. App module - The test app that you are using to test and develop your SDK, representing what your real app clients would have. Your app should have a dependency on the existing ad library module (runtime-aware SDK).
  2. Existing ad library module (runtime-aware SDK) - An Android library module containing your existing 'non-runtime-enabled' SDK logic, a statically linked SDK.
    • To start out, the capabilities can be split. For example, some code can be handled by your existing SDK, and some can be routed to the runtime-enabled SDK.
  3. Runtime-enabled ad library module - Contains your runtime-enabled SDK business logic. This can be created on Android Studio as an Android library module.
  4. Runtime-enabled ASB module - Defines the package data to bundle the runtime-enabled SDK code into an ASB.
    • It needs to be created manually using the com.android.privacy-sandbox-sdk type. You can do this by creating a new directory.
    • This module shouldn't contain any code and just an empty build.gradle file with dependencies to your runtime-enabled ad library module. The content of this file are defined in Prepare your SDK.
    • Remember to include this module in the settings.gradle file, and in the existing ad library module.

The project structure in this guide is a suggestion, you may choose a different structure for your SDK and apply the same technical principles. You always can create other additional modules to modularize the code in the app and the library modules.

Prepare your SDK

To prepare your project for runtime-enabled SDK development you need to first define some tooling and library dependencies:

  • SDK Runtime backwards compatibility libraries, which provide support for devices that don't have the Privacy Sandbox (Android 13 and below) (androidx.privacysandbox.sdkruntime:)
  • UI Libraries to support ad presentation (androidx.privacysandbox.ui:)
  • SDK developer tools to support SDK API declaration and shim-generation (androidx.privacysandbox.tools:)
  1. Add this flag to your project's gradle.properties file to enable the capability to create runtime-enabled SDKs.

    # This enables the Privacy Sandbox for your project on Android Studio.
    android.experimental.privacysandboxsdk.enable=true
    android.experimental.privacysandboxsdk.requireServices=false
    
  2. Modify your project's build.gradle to include the helper Jetpack libraries and other dependencies:

    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    buildscript {
        ext.kotlin_version = '1.9.10'
        ext.ksp_version = "$kotlin_version-1.0.13"
        ext.privacy_sandbox_activity_version = "1.0.0-alpha01"
        ext.privacy_sandbox_sdk_runtime_version = "1.0.0-alpha13"
        ext.privacy_sandbox_tools_version = "1.0.0-alpha09"
        ext.privacy_sandbox_ui_version = "1.0.0-alpha09"
        repositories {
            mavenCentral()
        }
        dependencies {
            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        }
    }
    
    plugins {
        id 'com.android.application' version '8.4.0-alpha13' apply false
        id 'com.android.library' version '8.4.0-alpha13' apply false
    
        // These two plugins do annotation processing and code generation for the sdk-implementation.
        id 'androidx.privacysandbox.library' version '1.0.0-alpha02' apply false
        id 'com.google.devtools.ksp' version "$ksp_version" apply false
    
        id 'org.jetbrains.kotlin.jvm' version '1.9.10' apply false
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
  3. Update the build.gradle file in the runtime-enabled ad library (RE SDK) module to include these dependencies.

    dependencies {
        // This allows Android Studio to parse and validate your SDK APIs.
        ksp "androidx.privacysandbox.tools:tools-apicompiler:$privacy_sandbox_tools_version"
    
        // This contains the annotation classes to decorate your SDK APIs.
        implementation "androidx.privacysandbox.tools:tools:$privacy_sandbox_tools_version"
    
        // This is runtime dependency required by the generated server shim code for
        // backward compatibility.
        implementation "androidx.privacysandbox.sdkruntime:sdkruntime-provider:$privacy_sandbox_sdk_runtime_version"
    
        // These are runtime dependencies required by the generated server shim code as
        // they use Kotlin.
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1"
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
    
        // This is the core part of the UI library to help with UI notifications.
        implementation "androidx.privacysandbox.ui:ui-core:$privacy_sandbox_ui_version"
    
        // This helps the SDK open sessions for the ad.
        implementation "androidx.privacysandbox.ui:ui-provider:$privacy_sandbox_ui_version"
    
        // This is needed if your SDK implements mediation use cases
        implementation "androidx.privacysandbox.ui:ui-client:$privacy_sandbox_ui_version"
    }
    
  4. Replace the build.gradle file in your runtime-enabled ASB module with the following:

    plugins {
        id 'com.android.privacy-sandbox-sdk'
    }
    
    android {
        compileSdk 34
        minSdk 21
    
        bundle {
            // This is the package name of the SDK that you want to publish.
            // This is used as the public identifier of your SDK.
            // You use this later on to load the runtime-enabled SDK
            packageName = '<package name of your runtime-enabled SDK>'
    
            // This is the version of the SDK that you want to publish.
            // This is used as the public identifier of your SDK version.
            setVersion(1, 0, 0)
    
            // SDK provider defined in the SDK Runtime library.
            // This is an important part of the future backwards compatibility
            // support, most SDKs won't need to change it.
            sdkProviderClassName = "androidx.privacysandbox.sdkruntime.provider.SandboxedSdkProviderAdapter"
    
            // This is the class path of your implementation of the SandboxedSdkProviderCompat class.
            // It's the implementation of your runtime-enabled SDK's entry-point.
            // If you miss this step, your runtime-enabled SDK will fail to load at runtime:
            compatSdkProviderClassName = "<your-sandboxed-sdk-provider-compat-fully-qualified-class-name>"
        }
    }
    
    dependencies {
        // This declares the dependency on your runtime-enabled ad library module.
        include project(':<your-runtime-enabled-ad-library-here>')
    }
    
  5. Update the build.gradle file in your existing ad library (RA SDK) module to include the following dependencies:

    dependencies {
        // This declares the client's dependency on the runtime-enabled ASB module.
        //  ⚠️ Important: We depend on the ASB module, not the runtime-enabled module.
        implementation project(':<your-runtime-enabled-asb-module-here>')
    
        // Required for backwards compatibility on devices where SDK Runtime is unavailable.
        implementation "androidx.privacysandbox.sdkruntime:sdkruntime-client:$privacy_sandbox_sdk_runtime_version"
    
        // This is required to display banner ads using the SandboxedUiAdapter interface.
        implementation "androidx.privacysandbox.ui:ui-core:$privacy_sandbox_ui_version"
        implementation "androidx.privacysandbox.ui:ui-client:$privacy_sandbox_ui_version"
    
        // This is required to use SDK ActivityLaunchers.
        implementation "androidx.privacysandbox.activity:activity-core:$privacy_sandbox_activity_version"
        implementation "androidx.privacysandbox.activity:activity-client:$privacy_sandbox_activity_version"
    }
    

Add SDK business logic

Implement the business logic of your SDK as you would regularly inside the runtime-enabled ad library module.

If you have an existing SDK that you're migrating, move as much of your business logic, interface, and system facing functions as you want at this stage, but account for a full migration in the future.

If you need access to storage, Google Play Advertising ID, or app set ID, read the following sections:

Use storage APIs in your SDK

SDKs in the SDK Runtime can no longer access, read, or write in an app's internal storage and the other way around.

The SDK Runtime is allocated its own internal storage area, separate from the app.

SDKs are able to access this separate internal storage using the file storage APIs on the Context object returned by the SandboxedSdkProvider#getContext().

SDKs can only use internal storage, so only internal storage APIs, such as Context.getFilesDir() or Context.getCacheDir() work. See more examples in Access from internal storage.

Access to external storage from SDK Runtime is not supported. Calling APIs to access external storage will either throw an exception or return null. The following list includes some examples:

You must use the Context returned by SandboxedSdkProvider.getContext() for storage. Using file storage API on any other Context object instance, such as the application context, is not guaranteed to work as expected in all situations.

The following code snippet demonstrates how to use storage in SDK Runtime:

class SdkServiceImpl(private val context: Context) : SdkService {
    override suspend fun getMessage(): String = "Hello from Privacy Sandbox!"

    override suspend fun createFile(sizeInMb: Int): String {
        val path = Paths.get(
            context.dataDir.path, "file.txt"
        )

        withContext(Dispatchers.IO) {
            Files.deleteIfExists(path)
            Files.createFile(path)
            val buffer = ByteArray(sizeInMb * 1024 * 1024)
            Files.write(path, buffer)
        }

        val file = File(path.toString())
        val actualFileSize: Long = file.length() / (1024 * 1024)
        return "Created $actualFileSize MB file successfully"
    }
}

Within the separate internal storage for each SDK Runtime, each SDK has its own storage directory. Per-SDK storage is a logical segregation of the SDK Runtime's internal storage which helps account for how much storage each SDK uses.

All internal storage APIs on the Context object return a storage path for each SDK.

Access the advertising ID provided by Google Play services

If your SDK needs access to the advertising ID provided by Google Play services use AdIdManager#getAdId() to retrieve the value asynchronously.

Access the app set ID provided by Google Play services

If your SDK needs access to the app set ID provided by Google Play services, use AppSetIdManager#getAppSetId() to retrieve the value asynchronously.

Declare SDK APIs

For your runtime-enabled SDK to be accessible outside of the runtime, you have to define APIs that clients (RA SDK or the client app) can consume.

Use annotations to declare these interfaces.

Annotations

SDK APIs need to be declared in Kotlin as interfaces and data classes using the following annotations:

Annotations
@PrivacySandboxService
  • Defines the entry-point to your RE SDK
  • Must be unique
@PrivacySandboxInterface
  • Enables further modularization and exposing interfaces
  • Can have multiple instances
@PrivacySandboxValue
  • Enables sending data across processes
  • Similar to immutable structs, which can return multiple values of different types
@PrivacySandboxCallback
  • Declares APIs with a callback
  • Provides a back channel to invoke client code

You need to define these interfaces and classes anywhere inside the runtime-enabled ad library module.

See the usage of these annotations in the following sections.

@PrivacySandboxService

@PrivacySandboxService
interface SdkService {
    suspend fun getMessage(): String

    suspend fun createFile(sizeInMb: Int): String

    suspend fun getBanner(request: SdkBannerRequest, requestMediatedAd: Boolean): SdkSandboxedUiAdapter?

    suspend fun getFullscreenAd(): FullscreenAd
}

@PrivacySandboxInterface

@PrivacySandboxInterface
interface SdkSandboxedUiAdapter : SandboxedUiAdapter

@PrivacySandboxValue

@PrivacySandboxValue
data class SdkBannerRequest(
    /** The package name of the app. */
    val appPackageName: String,
    /**
     *  An [SdkActivityLauncher] used to launch an activity when the banner is clicked.
     */
    val activityLauncher: SdkActivityLauncher,
    /**
     * Denotes if a WebView banner ad needs to be loaded.
     */
    val isWebViewBannerAd: Boolean
)

@PrivacySandboxCallback

@PrivacySandboxCallback
interface InAppMediateeSdkInterface {
    suspend fun show()
}

Supported types

Runtime-enabled SDK APIs support the following types:

  • All primitive types in the Java programming language (such as int, long, char, boolean, and so on)
  • String
  • Kotlin interfaces annotated with @PrivacySandboxInterface or @PrivacySandboxCallback
  • Kotlin data classes annotated with @PrivacySandboxValue
  • java.lang.List - all elements in the List must be one of the supported data types

There are some additional caveats:

  • Data classes annotated with @PrivacySandboxValue cannot contain fields of type @PrivacySandboxCallback
  • Return types cannot contain types annotated with @PrivacySandboxCallback
  • List cannot contain elements of types annotated with @PrivacySandboxInterface or @PrivacySandboxCallback

Asynchronous APIs

Since the SDK APIs always make a call to a separate process, we need to ensure that these calls don't block the client's calling thread.

To achieve this, all methods in the interfaces annotated with @PrivacySandboxService, @PrivacySandboxInterface and @PrivacySandboxCallback have to be explicitly declared as asynchronous APIs.

Asynchronous APIs can be implemented in Kotlin in two ways:

  1. Use suspend functions.
  2. Accept callbacks that get notified when the operation is complete, or of other events during the operation's progress. The return type of the function has to be a Unit.

Exceptions

The SDK APIs don't support any form of checked exceptions.

The generated shim code catches any runtime exceptions thrown by the SDK and throws them as a PrivacySandboxException to the client with information about the cause wrapped inside of it.

UI library

If you have interfaces that represent Ads, such as a banner, you also need to implement the SandboxedUiAdapter interface to enable opening sessions for the loaded ad.

These sessions form a side channel between the client and the SDK, and they fulfill two main purposes:

  • Receive notifications whenever a UI change occurs.
  • Notify the client of any changes in the UI presentation.

Since the client can use the interface annotated with @PrivacySandboxService to communicate with your SDK, any APIs for loading ads can be added to this interface.

When the client requests to load an ad, load the ad and return an instance of the interface implementing SandboxedUiAdapter. This allows the client to request opening sessions for that ad.

When the client requests to open a session, your runtime-enabled SDK can create an ad view using the ad response and the context provided.

To achieve, this, create a class that implements the SandboxedUiAdapter.Session interface and, when SandboxedUiAdapter.openSession() is called, ensure that you call client.onSessionOpened(), passing an instance of the Session class as a parameter.

class SdkSandboxedUiAdapterImpl(
   private val sdkContext: Context,
   private val request: SdkBannerRequest,
) : SdkSandboxedUiAdapter {
   override fun openSession(
       context: Context,
       windowInputToken: IBinder,
       initialWidth: Int,
       initialHeight: Int,
       isZOrderOnTop: Boolean,
       clientExecutor: Executor,
       client: SandboxedUiAdapter.SessionClient
   ) {
       val session = SdkUiSession(clientExecutor, sdkContext, request)
       clientExecutor.execute {
           client.onSessionOpened(session)
       }
   }
}

This class also receives notifications whenever a UI change occurs. You can use this class to resize the ad, or know when the configuration has changed, for example.

Learn more about UI Presentation APIs in the Runtime.

Activity support

To start SDK-owned activities from the Privacy Sandbox you need to modify the SDK API to receive an SdkActivityLauncher object, also provided by the UI library.

For example, the following SDK API should launch activities, so it expects SdkActivityLauncher parameter:

@PrivacySandboxInterface
interface FullscreenAd {
    suspend fun show(activityLauncher: SdkActivityLauncher)
}

SDK entry-point

The abstract class SandboxedSdkProvider encapsulates the API that the SDK Runtime uses to interact with the SDKs loaded into it.

A runtime-enabled SDK has to implement this abstract class to generate an entry point for the SDK runtime to be able to communicate with it.

For backward compatibility support, we have introduced the following classes:

Learn more about backward compatibility for the SDK Runtime.

The shim generation tools add another layer of abstraction: They generate an abstract class called AbstractSandboxedSdkProvider using the interface that you annotated with @PrivacySandboxService.

This class extends SandboxedSdkProviderCompat and is under the same package as your annotated interface.

// Auto-generated code.
abstract class AbstractSandboxedSdkProvider : SandboxedSdkProviderCompat {
    abstract fun createMySdk(context: Context): MySdk
}

This generated class exposes a single abstract factory method that takes in a Context and expects your entry-point annotated interface to be returned.

This method is named after your @PrivacySandboxService interface, prepending create to the name. For example, if your interface is named MySdk, the tools generate createMySdk.

To fully connect your entry point, you have to provide an implementation of your @PrivacySandboxService annotated interface in the runtime-enabled SDK to the generated AbstractSandboxedSdkProvider.

class MySdkSandboxedSdkProvider : AbstractSandboxedSdkProvider() {
    override fun createMySdk(context: Context): MySdk = MySdkImpl(context)
}

Changes to the ASB Module

You need to declare the fully-qualified class name of your implementation of the SandboxedSdkProviderCompat in the compatSdkProviderClassName field of your ASB module's build.gradle.

This is the class that you implemented in the previous step, and you would modify the build.gradle on your ASB Module as follows:

bundle {
    packageName = '<package name of your runtime-enabled SDK>'
    setVersion(1, 0, 0)

    // SDK provider defined in the SDK Runtime library.
    sdkProviderClassName = "androidx.privacysandbox.sdkruntime.provider.SandboxedSdkProviderAdapter"
    // This is the class that extends AbstractSandboxedSdkProvider,
    // MySdkSandboxProvider as per the example provided.
    compatSdkProviderClassName = "com.example.mysdk.MySdkSandboxProvider"
}

Step 2: Set up your development environment Step 4: Consume the runtime-enabled SDK