SDK Runtime developer guide

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.


Provide feedback

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

The following issue will be fixed in 2023:

  • The getAdId and getAppSetId 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:

  1. 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.

  2. 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.

  1. Add an app module to your project. This module serves as the client that drives the SDK.
  2. In your app module, enable the SDK Runtime, declare the necessary permissions and configure API-specific ad services.
  3. Add one library module to your project. This module contains your SDK code.
  4. In your SDK module, declare the necessary permissions. You don't need to configure API-specific ad services in this module.
  5. Remove the dependencies in your library module's build.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.
  6. 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 empty build.gradle file. The content of this file will be populated later in this guide.

  7. Add the following snippet to your gradle.properties file:

    android.experimental.privacysandboxsdk.enable=true
    
  8. 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

  1. 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 the dependencies 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>')
    }
    
  2. 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 extend SandboxedSdkProvider.

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 new SandboxedSdk object. The bound services guide provides different ways to provide IBinder. 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:

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 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.

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:

  1. Add the INTERNET and ACCESS_NETWORK_STATE permissions to your app's manifest:

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    
  2. 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 a SurfaceView 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;
    
  3. Check if the SDK Runtime process is available on the device.

    1. Check the SdkSandboxState constant (getSdkSandboxState()). SDK_SANDBOX_STATE_ENABLED_PROCESS_ISOLATION means the SDK Runtime is available.

    2. Check that calling loadSdk() is successful. It's successful if there are no exceptions thrown, and the receiver is the instance of the SandboxedSdk.

      • Call loadSdk() from the foreground. If it's called from the background a SecurityException will be thrown.

      • Check the OutcomeReceiver for an instance of SandboxedSdk to verify if a LoadSdkException was thrown. An exception indicates the SDK Runtime may not be available.

    If the SdkSandboxState or the loadSdk call fails, the SDK Runtime is not available and the call should fallback to the existing SDK.

  4. 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 the OutcomeReceiver<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
            }
        }
    

    When done showing the view, remember to release SurfacePackage by calling:

    surfacePackage.notifyDetachedFromWindow()
    
  5. In onCreate(), initialize the SdkSandboxManager, 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);
    }
    
  6. 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.

  7. 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:

  1. Open the Android Studio project for your client app.
  2. Go to Run > Edit Configurations. The Run/Debug Configuration window appears.
  3. Under Launch Options, set Launch to Specified Activity.
  4. Click the three dot menu next to Activity and select the Main Activity for your client.
  5. Click Apply and then OK.
  6. 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.

  1. 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.

  2. Install the APK on your device:

    adb install -t /path/to/your/standalone.apk
    
  3. In Android Studio, click Run > Edit Configurations. The Run/Debug Configuration window appears.

  4. Under Installation Options, set Deploy to Default APK.

  5. Click Apply and then OK.

  6. 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.

The SDK app process appears in a list view near the bottom
  of the dialog
Figure 1. The Choose process screen, where you can select the SDK app to debug.

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.