Learn More About the Alternative App Future

How to Run an Ad-Tech Android SDK in the New SDK Runtime Environment

Background

During early 2022, Google introduced a new model called “Android Privacy Sandbox”.The purpose of this model is to improve user privacy and enable an effective advertising experience for mobile applications.To achieve this goal, the Android Privacy Sandbox introduced new solutions: SDK Runtime and a set of privacy-preserving APIs.In this article I’ll focus on the SDK Runtime solution.

SDK Runtime - What has changed?

In the current model, SDKs operate in the host application sandbox.  This means, SDKs have access to the host application’s storage and memory, and inherits the same permissions and privileges as the host application. This allows a convenient integration between SDKs and apps, but it also raises concerns about potential user data collection and sharing.Additionally, app developers may not be familiar with the SDK’s functionality and the data it can access, making it difficult to control the data collection and sharing practices within their own applications.To address these issues, a new platform capability will be introduced in Android 13. It will enable 3rd party SDKs to operate in a separate and dedicated runtime environment, known as the SDK Runtime.This environment has specific permissions and data access rights designed for SDKs, which provides better protection and assurance regarding user data collection and sharing.

Impact on your SDK Project

SDKs in the SDK Runtime environment run in a separate process of the app.The app communicate with the SDK using interfaces. The interfaces cross a process boundary into the SDK Runtime process to call the SDK itself.The typical pattern of interaction between an app and an SDK can be summarized as follows:

1. The app initiates a call to the SDK via an interface, and provides it with the necessary callbacks

2. The SDK then processes the requests asynchronously and responds by utilizing the provided callbacks

This general process can be applied to any publisher-subscriber model, which enables an app to register for specific events in the SDK using callbacks. When these events occur, the relevant callbacks are triggered.

How to Setup

To test the SDK Runtime, you’ll need to perform the following steps, as described in the android developers guide:

1. Use the latest Canary version of Android Studio

2. Create an emulator with Play Store included and running API level TiramisuPrivacySandbox / or install the TiramisuPrivacySandbox image on a device

3. Configure Privacy Sandbox on the device/emulator:

End users can turn on/off the privacy sandbox. In the image preview, the privacy sandbox is disabled, by default. To enable it, run the following adb command and turn on the privacy sandbox toggle:

1
adb shell am start -n com.google.android.adservices.api/com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity

4. Run the following adb command:

1
adb shell device_config put adservices disable_sdk_sandbox false

Run a Sample of SDK Runtime

Google has provided us with a sample of privacy sandbox, you can find it on GitHub.I’ve used the java sample and modified the project to test the DT Exchange SDK in the Runtime environment.

Loading the SDK

Before any interaction with the SDK, the client app must load the SDK and register for a callback to be able to track the loading result:

1 2
final LoadSdkCallbackImpl callback = new LoadSdkCallbackImpl();
mSdkSandboxManager.loadSdk(<i>SDK_NAME</i>, new Bundle(), Runnable::run, callback);

Where LoadSdkCallbackImpl is a class that implements OutcomReceiver<SandboxedSdk, LoadSdkException>.

This class has 2 methods:

1
public void onResult(SandboxedSdk sandboxedSdk)

Which is called when the SDK is loaded successfully.

In onResult, the sandboxedSdk is received and we need to store it for communicating with the SDK.

1 2 3 4 5
private SandboxedSdk mSandboxedSdk; 
@Override 
public void onResult(SandboxedSdk sandboxedSdk) {    
    mSandboxedSdk = sandboxedSdk; 
    }

And

1
public void onError(LoadSdkException error)

Which is called when there is an error loading the SDK.

SDK Public APIs

In the current non-SDK Runtime model, SDKs developers expose a public API for their clients and clients can directly reach the SDK using the API.

In the new model, we (SDK developers) must create a cross-process API, such as aidl files.

Please note that in the official sample, there is a single aidl file ‘ISdkApi.aidl’ which is located in ‘example-aidl-library/src/main/java/com.example.exampleaidllibrary’.

To add more aidl files, e.g. for callbacks, rename the ‘java’ folder to ‘aidl’, and remove the following code from build.gradle

1 2 3 4 5
sourceSets {
main {
aidl.srcDirs = ['src']
}
}

Init the SDK

Once the SDK is loaded successfully, we can start communicating with it.

The first step is to initialize the SDK. To receive a callback with the initialization status, I created another aidl file to be used as a callback.

ISdkApi.aidl:

1 2 3 4 5
package com.example.exampleaidllibrary; 
import com.example.exampleaidllibrary.OnDTExchangeInitializedListener; 
interface ISdkApi {    
    void initialize(String appId, OnDTExchangeInitializedListener onDTExchangeInitializedListener); 
    }

OnDTExchangeInitializedListener.aidl:

1 2 3 4
package com.example.exampleaidllibrary; 
interface OnDTExchangeInitializedListener {    
    void onDTExchangeInitialized(String status); 
    }

In the client app, I created a class that extends OnDTExchangeInitializedListener.Stub and implemented the single method void onDTExchangeInitialized (String status);

Here is the code of this class:

1 2 3 4 5 6 7 8 9 10 11 12 13
package com.example.client; 
import com.example.exampleaidllibrary.OnDTExchangeplaceInitializedListener; 
public class InitSDKCallback extends OnDTExchangeInitializedListener.Stub {    
    private final MainActivity mActivity;    
    public InitSDKCallback(MainActivity activity) {        
        super();        
        mActivity = activity;   
        }    
        @Override    
        public void onDTExchangeInitialized(String status) {        
            mActivity.makeToast("DT Exchange SDK initialized with status: " + status);   
            } 
            }

To init the SDK, we must call the aidl API using the sandboxsdk binder:

1 2 3 4 5 6 7 8
IBinder binder = mSandboxedSdk.getInterface(); 
ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 
InitSDKCallback initSDKCallback = new InitSDKCallback(this); 
try {    
    sdkApi.initialize("appId", initSDKCallback); 
    } catch (RemoteException e) {    
        throw new RuntimeException(e); 
        }

When the SDK initialization process is finished, we receive a callback to the onDTExchangeInitialized method in class InitSDKCallback.

Loading a Banner

The next challenge was to render a banner from the DT SDK, to the client app, using the SDK Runtime environment.

In the SDK Runtime, an SDK cannot render a view directly in the client app. The SDK must prepare the view, and request to show it using a SurfaceView.

Due to the the cross-process communication, loadBanner method becomes asynchronous.  Therefore, I created a new aidl file to serve as a callback when the ad is ready to be shown.

OnAdReady.aidl

1 2 3 4
package com.example.exampleaidllibrary; 
interface OnAdReady {    
    void OnAdReady(String id); 
    }

I created a new class OnAdReadyCallback that extends OnAdReady.Stub, and pass an instance of it when calling loadBanner:

1 2 3 4 5 6 7 8
IBinder binder = mSandboxedSdk.getInterface();
ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder);
OnAdReadyCallback onAdReadyCallback = new OnAdReadyCallback(this);
try {
   sdkApi.loadBanner(onAdReadyCallback);
} catch (RemoteException e) {
   throw new RuntimeException(e);
}

loadBanner is implemented in the SDK.In our current, non-SDK runtime mode, when the ad is ready, the client app gets the ad’s controller, and the ad view layout, and uses them both to show the ad.

In the SDK Runtime, the SDK must notify the client that the ad is ready, and then the client app requests a surface from the sandbox manager and shows the ad.

Here are some code examples:

In the client app:

OnAdReadyCallback.java

1 2 3 4 5 6 7 8 9 10 11 12 13 14
package com.example.client; 
import com.example.exampleaidllibrary.OnAdReady; 
public class OnAdReadyCallback extends OnAdReady.Stub {    
    private final MainActivity mActivity;    
    public OnAdReadyCallback(MainActivity activity) {        
        super();        
        mActivity = activity;   
        }    
        @Override    
        public void OnAdReady(String id) {        
            mActivity.makeToast("DT Exchange OnAdReady");        
            mActivity.requestToShowBanner(id);   
            } 
            }

MainActivity.java

1 2 3 4 5 6 7 8 9 10 11 12
public void requestToShowBanner(String id) {    
    log("Getting SurfacePackage.");    
    new Handler(Looper.getMainLooper()).post(() -> {        
        Bundle params = new Bundle();        
        params.putInt(EXTRA_WIDTH_IN_PIXELS, mClientView.getWidth());        
        params.putInt(EXTRA_HEIGHT_IN_PIXELS, mClientView.getHeight());        
        params.putInt(EXTRA_DISPLAY_ID, getDisplay().getDisplayId());        
        params.putBinder(EXTRA_HOST_TOKEN, mClientView.getHostToken());        
        mSdkSandboxManager.requestSurfacePackage(                
            SDK_NAME, params, Runnable::run, new RequestSurfacePackageCallbackImpl());   
            }); 
            }

In the SDK implementation:

When calling requestSurfacePackage with the SDK_NAME identifier, it arrives to the SDK in a class that extends SandboxedSdkProvider.

In this class, the method getView is invoked:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
public class SdkProviderImpl extends SandboxedSdkProvider {  
    SdkApi sdkApi;  
    @SuppressLint("Override")  
    @Override  
    public SandboxedSdk onLoadSdk(Bundle params) {    
        sdkApi = new SdkApi(getContext());    
        return new SandboxedSdk(sdkApi); 
        }  
        @SuppressLint("Override")  
        @Override  
        public View getView(Context windowContext, Bundle bundle, int width, int height) {    
            FrameLayout frameLayout = new FrameLayout(windowContext) ;    
            frameLayout.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));    
            return sdkApi.mController.bindView(frameLayout); 
            } 
            }

When the view is ready, a callback in the client app is called (RequestSurfacePackageCallbackImpl) and notifies the client that SurfacePackage is ready to remote render a view from the SDK:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
private class RequestSurfacePackageCallbackImpl            
implements OutcomeReceiver<Bundle, RequestSurfacePackageException> {    
    @Override    
    public void onResult(Bundle response) {        
        log("Surface package ready");        
        makeToast("Surface Package Rendered!");        
        new Handler(Looper.getMainLooper()).post(() -> {            
            log("Setting surface package in the client view");            
            SurfacePackage surfacePackage = response.getParcelable(                
                EXTRA_SURFACE_PACKAGE, SurfacePackage.class);            
                mClientView.setChildSurfacePackage(surfacePackage);        
                });   
                }     
                @Override    
                public void onError(RequestSurfacePackageException error) {        
                    log("onSurfacePackageError" + error.getRequestSurfacePackageErrorCode() + "): "            
                    + error.getMessage());        
                    makeToast("Surface Package Failed! " + error.getMessage());   
                    } 
                    }

mClientView is a SurfaceView in the client app layout.

Here is the result of a banner ad showing inside a webview:

You Might Also Like
Small-Scale Data Pipeline: DuckDB vs. chDB vs. Spark
Private Service Connect for GKE Services with Gateway API
Harnessing Meta's Prophet for Advanced Anomaly Detection

Newsletter Sign-Up

Get our mobile expertise straight to your inbox.

Explore More

Small-Scale Data Pipeline: DuckDB vs. chDB vs. Spark
Unveiling APAC’s Award-Winning Campaigns: A Year of Innovation and Success
Private Service Connect for GKE Services with Gateway API