As you read through the Privacy Sandbox on Android documentation, use the Developer Preview or Beta button to select the program version that you're working with, as instructions may vary.
The SDK Runtime allows SDKs to run in a dedicated sandbox that's separate from the calling app. The SDK Runtime provides enhanced safeguards and guarantees around user data collection. This is done through a modified execution environment which limits data access rights and the set of allowed permissions. Learn more about the SDK Runtime in the design proposal.
The steps on this page guide you through the process of creating a runtime-enabled SDK that defines a web-based view that can be remotely rendered into a calling app.
Known limitations
For a list of in-progress capabilities for the SDK Runtime, view the release notes.
The following limitations are expected to be fixed in the next major Android platform release.
- Ad rendering within a scrollable view. For example,
RecyclerView
doesn't work properly.- You may experience jank on resize.
- The user touch scroll events is not passed to the runtime properly.
- Storage API
- Per-SDK storage is not available in Android 13.
The following issue will be fixed in 2023:
- The
getAdId
andgetAppSetId
APIs don't yet work properly since support for these have yet to be activated.
Before you begin
Before getting started, complete the following steps:
Set up your development environment for the Privacy Sandbox on Android. Tooling to support the SDK Runtime is under active development, so this guide will require you to use the latest Canary version of Android Studio. You can run this version of Android Studio in parallel to other versions you use, so please let us know if this requirement does not work for you.
Either install a system image onto a supported device or set up an emulator that includes support for the Privacy Sandbox on Android.
Set up your project in Android Studio
To try out the SDK Runtime, use a model that's similar to the client-server model. The main difference is that apps (the client) and SDKs (the "server") run on the same device.
- Add an app module to your project. This module serves as the client that drives the SDK.
- In your app module, enable the SDK Runtime, declare the necessary permissions and configure API-specific ad services.
- Add one library module to your project. This module contains your SDK code.
- In your SDK module, declare the necessary permissions. You don't need to configure API-specific ad services in this module.
- Remove the
dependencies
in your library module'sbuild.gradle
file that your SDK doesn't use. In most cases, you can remove all dependencies. You can do this by creating a new directory whose name corresponds to your SDK. Manually create a new module using the
com.android.privacy-sandbox-sdk
type. This is bundled with the SDK code to create an APK that can be deployed to your device. You can do this by creating a new directory whose name corresponds to your SDK. Add an emptybuild.gradle
file. The content of this file will be populated later in this guide.Add the following snippet to your
gradle.properties
file:android.experimental.privacysandboxsdk.enable=true
Download the Tiramisu (Extension Level 4) emulator image and create an emulator with this image that includes the Play Store.
Depending on whether you're an SDK developer or an app developer, you may have a different final setup than the one described in the preceding paragraph.
Install the SDK onto a test device, similarly to how you'd install an app, using either Android Studio or the Android Debug Bridge (ADB). To help you get started, we've created sample apps in the Kotlin and Java programming languages, which can be found in this GitHub repository. The README and manifest files have comments that describe what must be changed to run the sample in stable versions of Android Studio.
Prepare your SDK
Manually create a module-level directory. This serves as the wrapper around your implementation code to build the SDK APK. In the new directory, add a
build.gradle
file and populate it with the following snippet. Use a unique name for your runtime-enabled SDK (RE-SDK), and provide a version. Include your library module in thedependencies
section.plugins { id 'com.android.privacy-sandbox-sdk' } android { compileSdk 33 compileSdkExtension 4 minSdk 33 targetSdk 33 namespace = "com.example.example-sdk" bundle { packageName = "com.example.privacysandbox.provider" sdkProviderClassName = "com.example.sdk_implementation.SdkProviderImpl" setVersion(1, 0, 0) } } dependencies { include project(':<your-library-here>') }
Create a class in your implementation library to serve as an entry point for your SDK. The name of the class should map to the value of
sdkProviderClassName
and extendSandboxedSdkProvider
.
The entry point for your SDK extends SandboxedSdkProvider
. The
SandboxedSdkProvider
contains a Context
object for your SDK, which you can
access by calling getContext()
. This context must only be accessed once
onLoadSdk()
has been invoked.
To get your SDK app to compile, you need to override methods to handle the SDK lifecycle:
onLoadSdk()
Loads the SDK in the sandbox, and notifies the calling app when the SDK is ready to handle requests by passing its interface as a
IBinder
object that's wrapped inside a newSandboxedSdk
object. The bound services guide provides different ways to provideIBinder
. You have flexibility to choose your way, but it must be consistent for the SDK and the calling app.Using AIDL as an example, you should define an AIDL file to present your
IBinder
which is going to be shared and used by the app:// ISdkInterface.aidl interface ISdkInterface { // the public functions to share with the App. int doSomthing(); }
getView()
Creates and sets up the view for your ad, initializes the view the same way as any other Android view, and returns the view to be rendered remotely in a window of a given width and height in pixels.
The following code snippet demonstrates how to override these methods:
Kotlin
class SdkProviderImpl : SandboxedSdkProvider() { override fun onLoadSdk(params: Bundle?): SandboxedSdk { // Returns a SandboxedSdk, passed back to the client. The IBinder used // to create the SandboxedSdk object is used by the app to call into the // SDK. return SandboxedSdk(SdkInterfaceProxy()) } override fun getView(windowContext: Context, bundle: Bundle, width: Int, height: Int): View { val webView = WebView(windowContext) val layoutParams = LinearLayout.LayoutParams(width, height) webView.setLayoutParams(layoutParams) webView.loadUrl("https://developer.android.com/privacy-sandbox") return webView } private class SdkInterfaceProxy : ISdkInterface.Stub() { fun doSomething() { // Implementation of the API. } } }
Java
public class SdkProviderImpl extends SandboxedSdkProvider { @Override public SandboxedSdk onLoadSdk(Bundle params) { // Returns a SandboxedSdk, passed back to the client. The IBinder used // to create the SandboxedSdk object is used by the app to call into the // SDK. return new SandboxedSdk(new SdkInterfaceProxy()); } @Override public View getView(Context windowContext, Bundle bundle, int width, int height) { WebView webView = new WebView(windowContext); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(width, height); webView.setLayoutParams(layoutParams); webView.loadUrl("https://developer.android.com/privacy-sandbox"); return webView; } private static class SdkInterfaceProxy extends ISdkInterface.Stub { @Override public void doSomething() { // Implementation of the API. } } }
Test video players in the SDK Runtime
In addition to supporting banner ads, the Privacy Sandbox is committed to supporting video players running inside the SDK Runtime.
The flow for testing video players is similar to testing banner ads. Change the
getView()
method of your SDK's entry point to include a video player in the
returned View
object. Test all of the video player flows that you expect to be
supported by the Privacy Sandbox. Note that communication between the SDK and
the client app about the video's lifecycle is out of scope, so
feedback is not yet required for this functionality.
Your testing and feedback will ensure that the SDK Runtime supports all of the use cases of your preferred video player.
The following code snippet demonstrates how to return a simple video view that loads from a URL.
Kotlin
class SdkProviderImpl : SandboxedSdkProvider() { override fun getView(windowContext: Context, bundle: Bundle, width: Int, height: Int): View { val videoView = VideoView(windowContext) val layoutParams = LinearLayout.LayoutParams(width, height) videoView.setLayoutParams(layoutParams) videoView.setVideoURI(Uri.parse("https://test.website/video.mp4")) videoView.setOnPreparedListener { mp -> mp.start() } return videoView } }
Java
public class SdkProviderImpl extends SandboxedSdkProvider { @Override public View getView(Context windowContext, Bundle bundle, int width, int height) { VideoView videoView = new VideoView(windowContext); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(width, height); videoView.setLayoutParams(layoutParams); videoView.setVideoURI(Uri.parse("https://test.website/video.mp4")); videoView.setOnPreparedListener(mp -> { mp.start(); }); return videoView; } }
Using storage APIs in your SDK
SDKs in the SDK Runtime can no longer access, read, or write in an app's internal storage and vice versa. The SDK Runtime will be allocated its own internal storage area, which is guaranteed to be separate from the app.
SDKs will be 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()
will 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
. Several
examples:
- Accessing files using the Storage Access Framework will throw a
SecurityException
. getExternalFilsDir()
will always returnnull
.
In Android 13, all SDKs in the SDK Runtime will share the internal storage allocated for SDK Runtime. The storage will be persisted until the client app is uninstalled, or when client app data is cleaned up.
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
or in the future.
The following code snippet demonstrates how to use storage in SDK Runtime:
Kotlin
private static class SdkInterfaceStorage extends ISdkInterface.Stub { override fun doSomething() { val filename = "myfile" val fileContents = "content" try { getContext().openFileOutput(filename, Context.MODE_PRIVATE).use { it.write(fileContents.toByteArray()) } catch (e: Exception) { throw RuntimeException(e) } } } }
Java
private static class SdkInterfaceStorage extends ISdkInterface.Stub { @Override public void doSomething() { final filename = "myFile"; final String fileContents = "content"; try (FileOutputStream fos = getContext().openFileOutput(filename, Context.MODE_PRIVATE)) { fos.write(fileContents.toByteArray()); } catch (Exception e) { throw new RuntimeException(e); } } }
Per-SDK storage
Within the separate internal storage for each SDK Runtime, each SDK has it's 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.
In Android 13, only one API returns a path to the per-SDK storage:
Context#getDataDir()
.
On Android 14, all internal storage APIs on the Context
object return a
storage path for each SDK. You may need to enable this feature by running the
following adb command:
adb shell device_config put adservices sdksandbox_customized_sdk_context_enabled true
Access the advertising ID provided by Google Play services
If your SDK needs access to the advertising ID provided by Google Play services:
- Declare the
android.permission.ACCESS_ADSERVICES_AD_ID
permission in the SDK's manifest. - 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.
Update client apps
To call into an SDK that is running in the SDK Runtime, make the following changes to the calling client app:
Add the
INTERNET
andACCESS_NETWORK_STATE
permissions to your app's manifest:<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
In your app's activity that includes an ad, declare a reference to the
SdkSandboxManager
, a boolean to know whether the SDK is loaded, and aSurfaceView
object for remote rendering:Kotlin
private lateinit var mSdkSandboxManager: SdkSandboxManager private lateinit var mClientView: SurfaceView private var mSdkLoaded = false companion object { private const val SDK_NAME = "com.example.privacysandbox.provider" }
Java
private static final String SDK_NAME = "com.example.privacysandbox.provider"; private SdkSandboxManager mSdkSandboxManager; private SurfaceView mClientView; private boolean mSdkLoaded = false;
Check if the SDK Runtime process is available on the device.
Check the
SdkSandboxState
constant (getSdkSandboxState()
).SDK_SANDBOX_STATE_ENABLED_PROCESS_ISOLATION
means the SDK Runtime is available.Check that calling
loadSdk()
is successful. It's successful if there are no exceptions thrown, and the receiver is the instance of theSandboxedSdk
.Call
loadSdk()
from the foreground. If it's called from the background aSecurityException
will be thrown.Check the
OutcomeReceiver
for an instance ofSandboxedSdk
to verify if aLoadSdkException
was thrown. An exception indicates the SDK Runtime may not be available.
If the
SdkSandboxState
or theloadSdk
call fails, the SDK Runtime is not available and the call should fallback to the existing SDK.Define a callback class by implementing
OutcomeReceiver
to interact with the SDK in the runtime after it has been loaded. In the following example, the client uses a callback to wait until the SDK has been loaded successfully, then attempts to render a web view from the SDK. The callbacks are defined later in this step.Kotlin
private inner class LoadSdkOutcomeReceiverImpl private constructor() : OutcomeReceiver
{ override fun onResult(sandboxedSdk: SandboxedSdk) { mSdkLoaded = true val binder: IBinder = sandboxedSdk.getInterface() if (!binderInterface.isPresent()) { // SDK is not loaded anymore. return } val sdkInterface: ISdkInterface = ISdkInterface.Stub.asInterface(binder) sdkInterface.doSomething() Handler(Looper.getMainLooper()).post { val bundle = Bundle() bundle.putInt(SdkSandboxManager.EXTRA_WIDTH_IN_PIXELS, mClientView.getWidth()) bundle.putInt(SdkSandboxManager.EXTRA_HEIGHT_IN_PIXELS, mClientView.getHeight()) bundle.putInt(SdkSandboxManager.EXTRA_DISPLAY_ID, display!!.displayId) bundle.putInt(SdkSandboxManager.EXTRA_HOST_TOKEN, mClientView.getHostToken()) mSdkSandboxManager!!.requestSurfacePackage( SDK_NAME, bundle, { obj: Runnable -> obj.run() }, RequestSurfacePackageOutcomeReceiverImpl()) } } override fun onError(error: LoadSdkException) { // Log or show error. } } Java
import static android.app.sdksandbox.SdkSandboxManager.EXTRA_DISPLAY_ID; import static android.app.sdksandbox.SdkSandboxManager.EXTRA_HEIGHT_IN_PIXELS; import static android.app.sdksandbox.SdkSandboxManager.EXTRA_HOST_TOKEN; import static android.app.sdksandbox.SdkSandboxManager.EXTRA_WIDTH_IN_PIXELS; private class LoadSdkOutcomeReceiverImpl implements OutcomeReceiver
{ private LoadSdkOutcomeReceiverImpl() {} @Override public void onResult(@NonNull SandboxedSdk sandboxedSdk) { mSdkLoaded = true; IBinder binder = sandboxedSdk.getInterface(); if (!binderInterface.isPresent()) { // SDK is not loaded anymore. return; } ISdkInterface sdkInterface = ISdkInterface.Stub.asInterface(binder); sdkInterface.doSomething(); new Handler(Looper.getMainLooper()).post(() -> { Bundle bundle = new Bundle(); bundle.putInt(EXTRA_WIDTH_IN_PIXELS, mClientView.getWidth()); bundle.putInt(EXTRA_HEIGHT_IN_PIXELS, mClientView.getHeight()); bundle.putInt(EXTRA_DISPLAY_ID, getDisplay().getDisplayId()); bundle.putInt(EXTRA_HOST_TOKEN, mClientView.getHostToken()); mSdkSandboxManager.requestSurfacePackage( SDK_NAME, bundle, Runnable::run, new RequestSurfacePackageOutcomeReceiverImpl()); }); } @Override public void onError(@NonNull LoadSdkException error) { // Log or show error. } } To get back a remote view from the SDK in the runtime while calling
requestSurfacePackage()
, implement theOutcomeReceiver<Bundle, RequestSurfacePackageException>
interface:Kotlin
private inner class RequestSurfacePackageOutcomeReceiverImpl : OutcomeReceiver
{ fun onResult(@NonNull result: Bundle) { Handler(Looper.getMainLooper()) .post { val surfacePackage: SurfacePackage = result.getParcelable( EXTRA_SURFACE_PACKAGE, SurfacePackage::class.java) mRenderedView.setChildSurfacePackage(surfacePackage) mRenderedView.setVisibility(View.VISIBLE) } } fun onError(@NonNull error: RequestSurfacePackageException?) { // Error handling } } Java
import static android.app.sdksandbox.SdkSandboxManager.EXTRA_SURFACE_PACKAGE; private class RequestSurfacePackageOutcomeReceiverImpl implements OutcomeReceiver
{ @Override public void onResult(@NonNull Bundle result) { new Handler(Looper.getMainLooper()) .post( () -> { SurfacePackage surfacePackage = result.getParcelable( EXTRA_SURFACE_PACKAGE, SurfacePackage.class); mRenderedView.setChildSurfacePackage(surfacePackage); mRenderedView.setVisibility(View.VISIBLE); }); } @Override public void onError(@NonNull RequestSurfacePackageException error) { // Error handling } } In
onCreate()
, initialize theSdkSandboxManager
, necessary callbacks, and then make a request to load the SDK:Kotlin
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mSdkSandboxManager = applicationContext.getSystemService( SdkSandboxManager::class.java ) mClientView = findViewById(R.id.rendered_view) mClientView.setZOrderOnTop(true) val loadSdkCallback = LoadSdkCallbackImpl() mSdkSandboxManager.loadSdk( SDK_NAME, Bundle(), { obj: Runnable -> obj.run() }, loadSdkCallback ) }
Java
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mSdkSandboxManager = getApplicationContext().getSystemService( SdkSandboxManager.class); mClientView = findViewById(R.id.rendered_view); mClientView.setZOrderOnTop(true); LoadSdkCallbackImpl loadSdkCallback = new LoadSdkCallbackImpl(); mSdkSandboxManager.loadSdk( SDK_NAME, new Bundle(), Runnable::run, loadSdkCallback); }
To handle the case when the SDK sandbox process unexpectedly terminates, define an implementation for the
SdkSandboxProcessDeathCallback
interface:Kotlin
private inner class SdkSandboxLifecycleCallbackImpl() : SdkSandboxProcessDeathCallback { override fun onSdkSandboxDied() { // The SDK runtime process has terminated. To bring back up the // sandbox and continue using SDKs, load the SDKs again. val loadSdkCallback = LoadSdkOutcomeReceiverImpl() mSdkSandboxManager.loadSdk( SDK_NAME, Bundle(), { obj: Runnable -> obj.run() }, loadSdkCallback) } }
Java
private class SdkSandboxLifecycleCallbackImpl implements SdkSandboxProcessDeathCallback { @Override public void onSdkSandboxDied() { // The SDK runtime process has terminated. To bring back up // the sandbox and continue using SDKs, load the SDKs again. LoadSdkOutcomeReceiverImpl loadSdkCallback = new LoadSdkOutcomeReceiverImpl(); mSdkSandboxManager.loadSdk( SDK_NAME, new Bundle(), Runnable::run, loadSdkCallback); } }
To register this callback to receive information about when the SDK sandbox has terminated, add the following line at any time:
Kotlin
mSdkSandboxManager.addSdkSandboxProcessDeathCallback({ obj: Runnable -> obj.run() }, SdkSandboxLifecycleCallbackImpl())
Java
mSdkSandboxManager.addSdkSandboxProcessDeathCallback(Runnable::run, new SdkSandboxLifecycleCallbackImpl());
Because the state of the sandbox is lost when its process terminates, views that have been remotely rendered by the SDK might no longer work correctly. To continue interacting with SDKs, these views must be loaded again so that a new sandbox process is started. To monitor the status of the newly created sandbox process, re-register the callback using
addSdkSandboxProcessDeathCallback()
.Add a dependency on your SDK module to your client app's
build.gradle
:dependencies { ... implementation project(':<your-sdk-module>') ... }
Test your apps
To run your client app, install the SDK app and client app onto your test device using either Android Studio or the command line.
Deploy through Android Studio
When deploying through Android Studio, complete the following steps:
- Open the Android Studio project for your client app.
- Go to Run > Edit Configurations. The Run/Debug Configuration window appears.
- Under Launch Options, set Launch to Specified Activity.
- Click the three dot menu next to Activity and select the Main Activity for your client.
- Click Apply and then OK.
- Click Run
to install the client app and SDK on your test device.
Deploy on the command line
When deploying using the command line, complete the steps in the following list.
This section assumes that the name of your SDK app module is sdk-app
and that
the name of your client app module is client-app
.
From a command line terminal, build the Privacy Sandbox SDK APKs:
./gradlew :client-app:buildPrivacySandboxSdkApksForDebug
This outputs the location for the generated APKs. These APKs are signed with your local debug key. You need this path in the next command.
Install the APK on your device:
adb install -t /path/to/your/standalone.apk
In Android Studio, click Run > Edit Configurations. The Run/Debug Configuration window appears.
Under Installation Options, set Deploy to Default APK.
Click Apply and then OK.
Click Run to install the APK bundle on your test device.
Debug your apps
To debug the client app, click the Debug
button in Android Studio.
To debug the SDK app, go to Run > Attach to Process, which shows you a popup
screen (figure 1). Check the Show all processes box. In the list that
appears, look for a process called CLIENT_APP_PROCESS_sdk_sandbox
. Select this option and add breakpoints in the SDK app's code to
start debugging your SDK.

Start and stop the SDK runtime from the command line
To start the SDK runtime process for your app, use the following shell command:
adb shell cmd sdk_sandbox start [--user <USER_ID> | current] <CLIENT_APP_PACKAGE>
Similarly, to stop the SDK runtime process, run this command:
adb shell cmd sdk_sandbox stop [--user <USER_ID> | current] <CLIENT_APP_PACKAGE>
Limitations
For a list of in-progress capabilities for the SDK Runtime, view the release notes.
Code samples
The SDK Runtime and Privacy Preserving APIs Repository on GitHub contains a set of individual Android Studio projects to help you get started, including samples that demonstrate how to initialize and call the SDK Runtime.Report bugs and issues
Your feedback is a crucial part of the Privacy Sandbox on Android! Let us know of any issues you find or ideas for improving Privacy Sandbox on Android.