Key concepts | Set up your development environment | Build an RE SDK | Consume the RE SDK | Testing, and building for distribution |
Build a Runtime-Enabled SDK
You have to complete the following steps to build a runtime-enabled SDK:
- Set up your project structure
- Prepare your project and module dependencies
- Add your SDK business logic
- Define the SDK APIs
- Specify an entry-point for your SDK
Set up your project structure
We recommend that your project is organized into the following modules:
- 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).
- 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.
- Runtime-enabled ad library module - Contains your runtime-enabled SDK business logic. This can be created on Android Studio as an Android library module.
- 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:
)
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
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 }
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" }
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>') }
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:
- Accessing files using the Storage Access Framework throws a SecurityException.
getExternalFilsDir()
always returns null.
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 |
|
@PrivacySandboxInterface |
|
@PrivacySandboxValue |
|
@PrivacySandboxCallback |
|
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:
- Use suspend functions.
- 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:
SandboxedSdkProviderAdapter
, which extendsSandboxedSdkProvider
and handles SDK loading requests regardless of SDK Runtime availability. This is used internally, declared in the ASB Module.SandboxedSdkProviderCompat
, an abstract class that mimics the interface of theSandboxedSdkProvider
.
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 environmentStep 4: Consume the runtime-enabled SDK