构建和使用支持运行时的 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

构建支持运行时的 SDK

您必须完成以下步骤才能构建启用运行时的 SDK:

  1. 设置项目结构
  2. 准备项目和模块依赖项
  3. 添加 SDK 业务逻辑
  4. 定义 SDK API
  5. 为 SDK 指定入口点

设置项目结构

我们建议您将项目整理为以下模块:

  1. 应用模块 - 您用于测试和开发 SDK 的测试应用,代表真实应用客户端会有的内容。您的应用应依赖于现有的广告库模块(运行时感知 SDK)。
  2. 现有广告库模块(运行时感知型 SDK)- 一种 Android 库模块,包含您现有的“非运行时启用型”SDK 逻辑,即静态链接的 SDK。
    • 最初,您可以拆分功能。例如,某些代码可以由现有 SDK 处理,而某些代码可以路由到已启用运行时的 SDK。
  3. 支持运行时的广告库模块 - 包含支持运行时的 SDK 业务逻辑。可以在 Android Studio 中将其创建为 Android 库模块。
  4. 支持运行时的 ASB 模块 - 定义将支持运行时的 SDK 代码捆绑到 ASB 中的软件包数据。
    • 需要使用 com.android.privacy-sandbox-sdk 类型手动创建。为此,您可以创建一个新目录。
    • 此模块不应包含任何代码,而应仅包含一个空的 build.gradle 文件,其中包含对启用时广告库模块的依赖项。此文件的内容在准备 SDK 中定义。
    • 请务必在 settings.gradle 文件和现有广告库模块中包含此模块。

本指南中的项目结构仅为建议,您可以为 SDK 选择其他结构,并应用相同的技术原则。您始终可以创建其他附加模块,以模块化应用和库模块中的代码。

准备 SDK

如需为启用运行时的 SDK 开发准备项目,您需要先定义一些工具和库依赖项:

  • SDK 运行时向后兼容性库,可为没有 Privacy Sandbox 的设备(Android 13 及更低版本)提供支持 (androidx.privacysandbox.sdkruntime:)
  • 支持广告展示的界面库 (androidx.privacysandbox.ui:)
  • 支持 SDK API 声明和 shim 生成的 SDK 开发者工具 (androidx.privacysandbox.tools:)
  1. 将此标志添加到项目的 gradle.properties 文件,以启用创建支持运行时的 SDK 的功能。

    # This enables the Privacy Sandbox for your project on Android Studio.
    android.experimental.privacysandboxsdk.enable=true
    android.experimental.privacysandboxsdk.requireServices=false
    
  2. 修改项目的 build.gradle,以包含辅助 Jetpack 库和其他依赖项:

    // 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. 更新启用时的广告库 (RE SDK) 模块中的 build.gradle 文件,以包含这些依赖项。

    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. 启用运行时 ASB 的模块中的 build.gradle 文件替换为以下内容:

    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. 更新现有广告库 (RA SDK) 模块中的 build.gradle 文件,以包含以下依赖项:

    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"
    }
    

添加 SDK 业务逻辑

在启用运行时的广告库模块中,像往常一样实现 SDK 的业务逻辑。

如果您要迁移现有 SDK,可以在此阶段尽可能多地迁移业务逻辑、界面和面向系统的函数,但要考虑到将来会进行完全迁移。

如果您需要访问存储空间、Google Play 广告 ID 或应用组 ID,请参阅以下部分:

在 SDK 中使用存储 API

SDK 运行时中的 SDK 无法再访问、读取或写入应用的内部存储空间,反之亦然。

系统会为 SDK 运行时分配自己的内部存储区,该存储区与应用分隔开来。

SDK 将能够使用 SandboxedSdkProvider#getContext() 返回的 Context 对象上的文件存储 API 访问这一单独的内部存储空间。

SDK 只能使用内部存储空间,因此只能使用内部存储空间 API,例如 Context.getFilesDir()Context.getCacheDir()。如需查看更多示例,请参阅从内部存储空间访问

不支持从 SDK 运行时访问外部存储空间。调用 API 来访问外部存储空间时会抛出异常或返回 null。以下列表列出了一些示例:

您必须使用 SandboxedSdkProvider.getContext() 返回的 Context 进行存储。无法保证在所有情况下对任何其他 Context 对象实例(例如应用上下文)使用文件存储 API 能够有效运行。

以下代码段演示了如何在 SDK 运行时中使用存储空间:

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"
    }
}

在每个 SDK 运行时各自的内部存储空间中,每个 SDK 都有自己的存储目录。SDK 级存储空间是对 SDK 运行时内部存储空间的逻辑隔离,有助于解释每个 SDK 使用了多少存储空间。

Context 对象的所有内部存储 API 都会为每个 SDK 返回一个存储路径。

访问 Google Play 服务提供的广告 ID

如果您的 SDK 需要访问 Google Play 服务提供的广告 ID,请使用 AdIdManager#getAdId() 异步检索该值。

访问 Google Play 服务提供的应用组 ID

如果您的 SDK 需要访问 Google Play 服务提供的应用组 ID,请使用 AppSetIdManager#getAppSetId() 异步检索该值。

声明 SDK API

为了使启用运行时的 SDK 可在运行时之外访问,您必须定义客户端(RA SDK 或客户端应用)可以使用的 API。

使用注解来声明这些接口。

注释

需要在 Kotlin 中使用以下注解将 SDK API 声明为接口和数据类:

注释
@PrivacySandboxService
  • 定义 RE SDK 的入口点
  • 必须是唯一的
@PrivacySandboxInterface
  • 实现进一步模块化并公开接口
  • 可以有多个实例
@PrivacySandboxValue
  • 启用跨进程发送数据
  • 与不可变结构体类似,可以返回多种不同类型的值
@PrivacySandboxCallback
  • 声明具有回调的 API
  • 提供用于调用客户端代码的后向通道

您需要在启用运行时的广告库模块内的任意位置定义这些接口和类。

如需了解这些注释的用法,请参阅以下各部分。

@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()
}

支持的类型

支持运行时的 SDK API 支持以下类型:

  • Java 编程语言中的所有原初类型(例如 int、long、char、boolean 等)
  • 字符串
  • 使用 @PrivacySandboxInterface@PrivacySandboxCallback 注解的 Kotlin 接口
  • 使用 @PrivacySandboxValue 注释的 Kotlin 数据类
  • java.lang.List - List 中的所有元素都必须是受支持的数据类型之一

还有一些其他注意事项

  • 使用 @PrivacySandboxValue 注释的数据类不能包含 @PrivacySandboxCallback 类型的字段
  • 返回类型不能包含带有 @PrivacySandboxCallback 注释的类型
  • 列表不能包含带有 @PrivacySandboxInterface@PrivacySandboxCallback 注释的类型的元素

异步 API

由于 SDK API 始终会调用单独的进程,因此我们需要确保这些调用不会阻塞客户端的调用线程。

为此,使用 @PrivacySandboxService@PrivacySandboxInterface@PrivacySandboxCallback 注释添加注释的接口中的所有方法都必须明确声明为异步 API。

在 Kotlin 中,可以通过两种方式实现异步 API:

  1. 使用挂起函数
  2. 接受在操作完成时或在操作进行期间的其他事件发生时收到通知的回调。函数的返回类型必须是 Unit。

异常

SDK API 不支持任何形式的检查型异常。

生成的 shim 代码会捕获 SDK 抛出的任何运行时异常,并将其作为 PrivacySandboxException 抛给客户端,其中包含有关异常原因的信息。

界面库

如果您有表示广告(例如横幅)的接口,还需要实现 SandboxedUiAdapter 接口,以便为已加载的广告启用打开会话。

这些会话在客户端和 SDK 之间形成一个边信道,主要用于以下两个目的:

  • 每当界面发生变化时,您都会收到通知。
  • 通知客户端界面呈现方面的任何变化。

由于客户端可以使用带有 @PrivacySandboxService 注释的接口与您的 SDK 进行通信,因此任何用于加载广告的 API 都可以添加到此接口。

当客户端请求加载广告时,加载广告并返回实现 SandboxedUiAdapter 的接口的实例。这样,客户端便可请求为相应广告打开会话。

当客户端请求打开会话时,启用运行时功能的 SDK 可以使用广告响应和提供的上下文创建广告视图。

为此,请创建一个实现 SandboxedUiAdapter.Session 接口的类,并在调用 SandboxedUiAdapter.openSession() 时,确保您调用 client.onSessionOpened(),并传入 Session 类的实例作为参数。

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)
       }
   }
}

此类还会在每次发生界面更改时收到通知。您可以使用此类来调整广告大小,或了解配置何时发生更改。

详细了解运行时的界面呈现 API。

activity 支持

如需从 Privacy Sandbox 启动 SDK 拥有的 activity,您需要修改 SDK API 以接收 SdkActivityLauncher 对象,该对象也由界面库提供。

例如,以下 SDK API 应该启动 activity,因此需要 SdkActivityLauncher 参数:

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

SDK 入口点

抽象类 SandboxedSdkProvider 封装了 SDK 运行时用于与加载到其中的 SDK 进行交互的 API。

启用运行时的 SDK 必须实现此抽象类才能生成入口点,以便 SDK 运行时能够与该 SDK 进行通信。

为了支持向后兼容性,我们引入了以下类:

详细了解 SDK 运行时的向后兼容性。

shim 生成工具会添加另一层抽象:它们会使用您添加了 @PrivacySandboxService 注解的接口生成一个名为 AbstractSandboxedSdkProvider 的抽象类。

此类扩展了 SandboxedSdkProviderCompat,并且与带注释的接口位于同一软件包下。

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

此生成的类会公开一个抽象工厂方法,该方法接受 Context 并期望返回带注释的入口点接口。

此方法以您的 @PrivacySandboxService 接口命名,并在名称前添加 create。例如,如果您的接口名为 MySdk,则工具会生成 createMySdk

如需完全连接入口点,您必须在启用运行时的 SDK 中为生成的 AbstractSandboxedSdkProvider 提供 @PrivacySandboxService 注释接口的实现。

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

对 ASB 模块的更改

您需要在 ASB 模块的 build.gradle 的 compatSdkProviderClassName 字段中声明 SandboxedSdkProviderCompat 实现的完全限定类名称。

这是您在上一步中实现的类,您需要按如下方式修改 ASB 模块中的 build.gradle:

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"
}

第 2 步:设置开发环境 第 4 步:使用启用运行时功能的 SDK