SDK 執行階段開發人員指南

詳閱 Android 版 Privacy Sandbox 說明文件時,請利用「開發人員預覽版」或「Beta 版」按鈕選取您要使用的計畫版本,因為兩者的操作說明可能不盡相同。


提供意見

SDK 執行階段可讓 SDK 在呼叫應用程式以外的專屬沙箱中執行。SDK 執行階段會針對使用者資料收集作業提供強化的保護措施和保證。這是透過改進後的執行環境來達成,這個環境會限制資料存取權和獲得授予的權限。如要進一步瞭解 SDK 執行階段,請參閱設計提案

此頁面將逐步引導您建立啟用執行階段的 SDK,以定義可遠端轉譯至通話應用程式的網頁式檢視畫面。

已知限制

如要查看目前針對 SDK 執行階段開發的功能,請參閱版本資訊。

我們預計將在下一個主要 Android 平台版本中修正下列限制。

  • 在可捲動的檢視畫面中顯示廣告。舉例來說,RecyclerView 無法正常運作。
    • 調整大小後可能會發生卡頓情形。
    • 使用者觸控捲動事件不會正確傳遞至執行階段。
  • Storage API

下列問題將在 2023 年修正:

  • getAdIdgetAppSetId API 的支援功能尚未啟用,因此這些 API 還無法正常運作。

事前準備

在開始之前,請先完成下列步驟:

  1. 在 Android 裝置上為 Privacy Sandbox 設定開發環境。支援 SDK 執行階段的工具現處於積極開發階段,因此本指南要求您使用最新的 Android Studio Canary 版本。您可以同時執行這個版本的 Android Studio 與目前使用的其他版本,因此如果無法達成這項要求,請告訴我們

  2. 將系統映像檔安裝到支援的裝置上,或者設定支援 Android 版 Privacy Sandbox 的模擬器

在 Android Studio 中設定專案

如要試用 SDK 執行階段,請使用與用戶端伺服器模型類似的模型。主要差別在於應用程式 (用戶端) 和 SDK (「伺服器」) 是在同一部裝置上執行。

  1. 在專案中新增應用程式模組。這個模組可做為驅動 SDK 的用戶端。
  2. 在應用程式模組中,啟用 SDK 執行階段宣告必要的權限,並設定 API 專屬廣告服務
  3. 在專案中新增程式庫模組。這個模組會包含 SDK 程式碼。
  4. 在 SDK 模組中宣告必要權限。您不需要在這個模組中設定 API 專屬廣告服務。
  5. 從程式庫模組的 build.gradle 檔案中移除 SDK 未使用的 dependencies。在大多數情況下,您可以移除所有依附元件。方法是建立名稱與您的 SDK 相對應的新目錄。
  6. 使用 com.android.privacy-sandbox-sdk 類型手動建立新模組。其與 SDK 程式碼組合在一起,產生一個可以部署至您的裝置的 APK。方法是建立名稱與您的 SDK 相對應的新目錄。新增空白的 build.gradle 檔案。本指南稍後會填入這個檔案的內容。

  7. gradle.properties 檔案中新增下列程式碼片段:

    android.experimental.privacysandboxsdk.enable=true
    
  8. 下載「TiramisuPrivacySandbox」TiramisuPrivacySandbox模擬器映像檔,並使用這個含有 Play 商店的映像檔建立模擬器。

視您是 SDK 開發人員還是應用程式開發人員而定,您選用的最終設定可能會與上一段所述的設定不同。

利用 Android Studio 或 Android Debug Bridge (ADB),將 SDK 安裝到測試裝置中,方法與安裝應用程式類似。為了協助您快速上手,我們以 Kotlin 和 Java 程式語言建立了一些範例應用程式,如要查看,請前往這個 GitHub 存放區。如要瞭解在 Android Studio 穩定版中執行範例所需的變更項目,請參閱 README 和資訊清單檔案的註解。

準備 SDK

  1. 手動建立模組層級目錄。這可做為實作程式碼的相關包裝函式,用來建構 SDK APK。在新目錄中新增 build.gradle 檔案,並填入下列程式碼片段。請為啟用執行階段的 SDK (RE-SDK) 設定專屬的名稱,並提供版本。將程式庫模組包含至 dependencies 區段中。

    plugins {
        id 'com.android.privacy-sandbox-sdk'
    }
    
    android {
        compileSdkPreview 'TiramisuPrivacySandbox'
        minSdkPreview 'TiramisuPrivacySandbox'
        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. 在實作程式庫中建立類別,做為 SDK 的進入點。類別的名稱應對應至 sdkProviderClassName 的值,並擴充 SandboxedSdkProvider

SDK 的進入點會擴充 SandboxedSdkProviderSandboxedSdkProvider 包含 SDK 的 Context 物件,只要呼叫 getContext() 即可存取這個物件。這個環境只能在叫用 onLoadSdk() 後存取。

如要讓 SDK 應用程式進行編譯,您必須透過覆寫多個方法來處理 SDK 生命週期:

onLoadSdk()

在沙箱中載入 SDK,並在 SDK 準備好處理要求時,以包裝在新 SandboxedSdk 物件內的 IBinder 物件形式傳遞其介面,藉此通知呼叫應用程式。繫結服務指南包含多種提供 IBinder 的方式。您可以彈性選擇要採用的方式,但 SDK 和呼叫應用程式採用的方式必須一致。

AIDL 為範例,您必須定義 AIDL 檔案,用於表示應用程式將提供及使用的 IBinder

// ISdkInterface.aidl
interface ISdkInterface {
    // the public functions to share with the App.
    int doSomething();
}
getView()

建立及設定廣告的檢視畫面,採用與任何其他 Android 檢視畫面相同的方式來初始化該檢視畫面,並傳回可用於在指定寬度和高度像素數的視窗中進行遠端轉譯的檢視畫面。

以下程式碼片段演示了如何覆寫這些方法:

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

SdkSandboxController

SdkSandboxController 是可用於 SDK 的結構定義式系統服務包裝函式。SDK 可使用從 SandboxedSdkProvider#getContext() 接收的結構定義,並對此結構定義叫用 context.getSystemService(SdkSandboxController.class),這樣就能擷取此包裝函式。這個控制器的 API 可協助 SDK 與 Privacy Sandbox 中的資訊互動,並取得這些資訊。

如果 SandboxedSdkContext 不是用於存取作業的結構定義,控制器中的大多數 API 都會擲回例外狀況。我們打算停止向其他結構定義提供這個服務包裝函式。SdkSandboxController 的用途是給 SDK 供應商使用,並不建議應用程式開發人員使用。

SDK 對 SDK 的通訊

執行階段中的 SDK 應該要能相互通訊,以便支援中介服務和相關用途。本文所述的工具組提供可與 SDK 搭配使用的介面,這些 SDK 也可供沙箱中的其他 SDK 使用。這項 SDK 執行階段實作程序是實現 SDK 對 SDK 通訊的第一步,可能尚未涵蓋 Privacy Sandbox 內中介服務的所有用途。

SdkSandboxController 中的 getSandboxedSdks() API 為 Privacy Sandbox 內所有載入的 SDK 提供了 SandboxedSdk 類別。SandboxedSdk 物件含有 SDK 和 sdkInterface 的相關詳細資料,可讓用戶端與其通訊。

Privacy Sandbox 中的 SDK 應該使用類似下方的程式碼片段,與其他 SDK 通訊。在此假設編寫這段程式碼的用意是讓「SDK1」能與「SDK2」通訊,而且這兩個 SDK 都是由 Privacy Sandbox 中的應用程式載入。

SdkSandboxController controller = mSdkContext
    .getSystemService(SdkSandboxController.class);
List<SandboxedSdk> sandboxedSdks = controller.getSandboxedSdks();
SandboxedSdk sdk2 = sandboxedSdks.stream().filter( // The SDK it wants to
    // connect to, based on SDK name or SharedLibraryInfo.
try {
    IBinder binder = sdk2.getInterface();
    ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder);
    // Call API on SDK2
    message = sdkApi.getMessage();
    } catch (RemoteException e) {
        throw new RuntimeException(e);
}

在上述範例中,SDK1 新增 SDK2 的 AIDL 程式庫做為依附元件。此用戶端 SDK 含有 AIDL 產生的 Binder 程式碼。這兩個 SDK 都應該匯出此 AIDL 程式庫。當應用程式與 Privacy Sandbox 中的 SDK 通訊時,也會出現相同的預期行為。

日後推出的更新將添加支援功能,以自動產生的方式在 SDK 之間共用介面。

如果是執行階段中的 SDK,可能需要與尚未支援執行階段的應用程式依附元件和廣告 SDK 通訊。

SdkSandboxManager 中的 registerAppOwnedSdkSandboxInterface() API 可讓未支援執行階段的 SDK 向平台註冊介面。SdkSandboxController 中的 getAppOwnedSdkSandboxInterfaces() API 會為所有已註冊的靜態連結 SDK 提供 AppOwnedSdkSandboxInterface

以下範例說明如何註冊介面,讓這些介面為已支援執行階段的 SDK 支援通訊功能:

// Register AppOwnedSdkSandboxInterface
mSdkSandboxManager.registerAppOwnedSdkSandboxInterface(
    new AppOwnedSdkSandboxInterface(
        APP_OWNED_SDK_NAME, (long) APP_OWNED_SDK_VERSION, new AppOwnedSdkApi())
    );

以下範例說明如何檢測與未支援執行階段的 SDK 通訊的情形:

// Get AppOwnedSdkSandboxInterface
List<AppOwnedSdkSandboxInterface> appOwnedSdks = mSdkContext
        .getSystemService(SdkSandboxController.class)
        .getAppOwnedSdkSandboxInterfaces();
    AppOwnedSdkSandboxInterface appOwnedSdk = appOwnedSdks.stream()
        .filter(s -> s.getName().contains(APP_OWNED_SDK_NAME))
        .findAny()
        .get();
    IAppOwnedSdkApi appOwnedSdkApi =
        IAppOwnedSdkApi.Stub.asInterface(appOwnedSdk.getInterface());
    message = appOwnedSdkApi.getMessage();

未支援執行階段的廣告 SDK 可能無法自行註冊,因此建議您建立中介 SDK 來處理註冊事宜,並將合作夥伴或應用程式 SDK 納入為直接依附元件。這個中介 SDK 會在以下兩者之間建立通訊:未支援執行階段的 SDK 和依附元件,以及擔任轉接程式且已支援執行階段的中介服務。

活動支援

支援執行階段的 SDK 無法在資訊清單檔案中新增活動標記,也無法直接啟動自己的活動。如要將存取權提供給 Activity 物件,需要註冊 SdkSandboxActivityHandler 及啟動沙箱活動:

1. 註冊 SdkSandboxActivityHandler

使用 SdkSandboxController#registerSdkSandboxActivityHandler(SandboxedActivityHandler) 註冊 SdkSandboxActivityHandler 例項

API 會註冊該物件,並傳回可識別所傳遞 SdkSandboxActivityHandlerIBinder 物件。

public interface SdkSandboxActivityHandler {
    void onActivityCreated(Activity activity);
}

API 會註冊該物件,並傳回可識別所傳遞 SdkSandboxActivityHandlerIBinder 物件。

2. 啟動沙箱活動

SDK 會將用於找出所註冊 SdkSandboxActivityHandler 而傳回的權杖傳遞至用戶端應用程式。接著,用戶端應用程式會呼叫 SdkSandboxManager#startSdkSandboxActivity(Activity, Binder),傳遞用於啟動沙箱的活動,以及可找出所註冊 SdkSandboxActivityHandler 的權杖。

這個步驟會啟動新的平台活動,且該活動會在與要求 SDK 相同的 SDK 執行階段中執行。

當活動啟動時,系統會在 Activity#OnCreate(Bundle) 執行作業中呼叫 SdkSandboxActivityHandler#onActivityCreated(Activity) 來通知 SDK。

舉例來說,如果具備 Activity 物件的存取權,呼叫端可以呼叫 Activity#setContentView(View) 來設定 contentView 檢視畫面。

如要註冊生命週期回呼,請使用 Activity#registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks)

如要將 OnBackInvokedCallback 註冊至傳遞的活動,請使用 Activity#getOnBackInvokedDispatcher().registerOnBackInvokedCallback(Int, OnBackInvokedCallback)

在 SDK 執行階段中測試影片播放器

除了支援橫幅廣告外,Privacy Sandbox 也致力於支援在 SDK 執行階段內執行的影片播放器。

測試影片播放器的流程與測試橫幅廣告類似。請變更 SDK 進入點的 getView() 方法,將影片播放器加入傳回的 View 物件中。接著,對您預期 Privacy Sandbox 會支援的所有影片播放器流程展開測試。請注意,SDK 和用戶端應用程式之間的影片生命週期相關通訊已超出測試範圍,因此目前還不需要對此功能提供意見回饋。

測試和意見回饋可確保 SDK 執行階段支援您偏好的影片播放器的所有用途。

以下程式碼片段示範如何傳回從網址載入的簡易影片檢視畫面。

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

在 SDK 中使用 Storage API

SDK 執行階段中的 SDK 無法再存取、讀取或寫入應用程式的內部儲存空間,反之亦然。SDK 執行階段會分配到專屬的內部儲存區域,該區域保證與應用程式分隔開來。

SDK 可以對 SandboxedSdkProvider#getContext() 傳回的 Context 物件使用檔案儲存空間 API,存取這個獨立的內部儲存空間。SDK 只能使用內部儲存空間,因此只有內部儲存空間 API (例如 Context.getFilesDir()Context.getCacheDir()) 才能運作。如需更多範例,請參閱「從內部儲存空間存取」一文。

無法透過 SDK 執行階段存取外部儲存空間。呼叫 API 以存取外部儲存空間時,系統會擲回例外狀況或傳回 null。以下提供幾個範例:

在 Android 13 中,SDK 執行階段中的所有 SDK 都會共用分配給 SDK 執行階段的內部儲存空間。系統會保留儲存空間,直到用戶端應用程式解除安裝,或用戶端應用程式資料遭到清除為止。

您必須使用 SandboxedSdkProvider.getContext() 傳回的 Context 做為儲存空間。在任何其他 Context 物件執行個體 (例如應用程式環境) 上使用檔案儲存空間 API,不保證會在所有情境或日後版本中順利運作。

下列程式碼片段示範如何在 SDK 執行階段中使用儲存空間:

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

}

    

SDK 級儲存空間

在每個 SDK 執行階段的獨立內部儲存空間中,每個 SDK 都有專屬的儲存空間目錄。SDK 級儲存空間是 SDK 執行階段內部儲存空間的邏輯區隔,有助於解釋每個 SDK 使用多少儲存空間。

在 Android 13 中,只有一個 API 會傳回個別 SDK 儲存空間的路徑:Context#getDataDir()

在 Android 14 中,Context 物件的所有內部儲存空間 API 都會為每個 SDK 傳回儲存空間路徑。您可能需要執行下列 ADB 指令,才能啟用這項功能:

adb shell device_config put adservices sdksandbox_customized_sdk_context_enabled true

讀取用戶端的 SharedPreferences

用戶端應用程式可以選擇與 SdkSandbox 共用其 SharedPreferences 中的一組金鑰。SDK 可使用 SdkSanboxController#getClientSharedPreferences() API 讀取從用戶端應用程式同步處理的資料。這個 API 傳回的 SharedPreferences 僅可用於讀取。請不要執行寫入作業。

存取 Google Play 服務提供的廣告 ID

如果 SDK 需要存取 Google Play 服務提供的廣告 ID,請執行以下操作:

  • 在 SDK 的資訊清單中宣告 android.permission.ACCESS_ADSERVICES_AD_ID 權限。
  • 使用 AdIdManager#getAdId() 以非同步方式擷取值。

存取 Google Play 服務提供的應用程式組 ID

如果 SDK 需要存取 Google Play 服務提供的應用程式組 ID,請執行以下操作:

  • 使用 AppSetIdManager#getAppSetId() 以非同步方式擷取值。

更新用戶端應用程式

如要呼叫在 SDK 執行階段中執行的 SDK,請對呼叫用戶端應用程式進行下列變更:

  1. 在應用程式資訊清單中新增 INTERNETACCESS_NETWORK_STATE 權限:

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    
  2. 在含有廣告的應用程式活動中,宣告 SdkSandboxManager 的參照、用於瞭解 SDK 是否已載入的布林值,以及用於遠端轉譯的 SurfaceView 物件:

    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. 檢查裝置是否提供 SDK 執行階段程序。

    1. 檢查 SdkSandboxState 常數 (getSdkSandboxState())。SDK_SANDBOX_STATE_ENABLED_PROCESS_ISOLATION 表示 SDK 執行階段可供使用。

    2. 檢查是否成功呼叫 loadSdk()。如果未擲回例外狀況,且接收器是 SandboxedSdk 的執行個體,則表示呼叫成功。

      • 從前景呼叫 loadSdk()。如果從背景呼叫,系統會擲回 SecurityException

      • 檢查 OutcomeReceiver 是否有 SandboxedSdk 的執行個體,驗證是否已擲回 LoadSdkException。例外狀況表示 SDK 執行階段無法使用。

    如果 SdkSandboxStateloadSdk 呼叫失敗,則無法使用 SDK 執行階段,因此呼叫應回退至現有 SDK。

  4. 實作 OutcomeReceiver 來定義回呼類別,以便載入資料在執行階段中與 SDK 互動。在以下範例中,用戶端使用回呼來等待 SDK 成功載入,接著嘗試轉譯來自 SDK 的網頁檢視畫面。稍後會在此步驟中定義回呼。

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

    如要在執行階段從 SDK 取回遠端檢視畫面,同時呼叫 requestSurfacePackage(),請實作 OutcomeReceiver<Bundle, RequestSurfacePackageException> 介面:

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

    檢視畫面顯示完畢後,請記得呼叫以下指令來釋出 SurfacePackage

    surfacePackage.notifyDetachedFromWindow()
    
  5. onCreate() 中,初始化 SdkSandboxManager、必要的回呼,然後提出載入 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. 應用程式可以選擇與沙箱分享預設 SharedPreferences 的特定金鑰。為此,請在任何 SdkSandbox 管理員的執行個體上呼叫 SdkSandboxManager#addSyncedSharedPreferencesKeys(Set<String>keys) 方法。應用程式通知 SdkSandboxManager 要同步處理哪個金鑰後,SdkSandboxManager 會將這些金鑰的值同步處理至沙箱和 SDK,然後使用 SdkSandboxController#getClientSharedPreferences 讀取這些金鑰。詳情請參閱「讀取用戶端的 SharedPreferences」。

    應用程式重新啟動後,系統不會保留已同步處理的金鑰組合,而且在沙箱重新啟動時會清除已同步到沙箱的資料。因此,應用程式每次啟動時都必須呼叫 addSyncedSharedPreferencesKeys 來啟動同步處理作業。

    如要修改正在同步處理的金鑰組合,請呼叫 SdkSandboxManager#removeSyncedSharedPreferencesKeys(Set<String>keys) 來移除金鑰。如要查看目前正在同步處理的一組金鑰,請使用 SdkSandboxManager#getSyncedSharedPreferencesKeys()

    建議您盡量縮減一組金鑰,並且只在必要時使用。如要將資訊傳送至 SDK 用於一般用途,請使用 SandboxedSdk 介面直接與 SDK 通訊。使用這些 API 的可能情境是,如果應用程式使用同意聲明管理平台 (CMP) SDK,則沙箱中的 SDK 會讀取 CMP SDK 在預設 SharedPreferences 中儲存的資料。

    Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        …
        // At some point, initiate the set of keys for synchronization with sandbox
        mSdkSandboxManager.addSyncedSharedPreferencesKeys(Set.of("foo", "bar"));
    }
    
    

    Java

    @Override
        protected void onCreate(Bundle savedInstanceState) {
        …
        // At some point, initiate the set of keys for synchronization with sandbox
        mSdkSandboxManager.addSyncedSharedPreferencesKeys(Set.of("foo", "bar"));
    }
    
    
  7. 如要處理 SDK 沙箱程序意外終止的情況,請定義 SdkSandboxProcessDeathCallback 介面的實作:

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

    如要註冊這個回呼以接收 SDK 沙箱終止時間的相關資訊,您隨時可以新增下列程式碼:

    Kotlin

        mSdkSandboxManager.addSdkSandboxProcessDeathCallback({ obj: Runnable -> obj.run() },
                SdkSandboxLifecycleCallbackImpl())
    

    Java

        mSdkSandboxManager.addSdkSandboxProcessDeathCallback(Runnable::run,
                new SdkSandboxLifecycleCallbackImpl());
    

    由於沙箱狀態已在沙箱程序終止時遺失,因此由 SDK 遠端轉譯的檢視畫面可能無法再正常運作。如要繼續與 SDK 互動,您必須再次載入這些檢視畫面,藉此啟動新的沙箱程序。

  8. 將 SDK 模組的依附元件新增至用戶端應用程式的 build.gradle

    dependencies {
        ...
        implementation project(':<your-sdk-module>')
        ...
    }

測試應用程式

如要執行用戶端應用程式,請使用 Android Studio 或指令列在測試裝置上安裝 SDK 應用程式和用戶端應用程式。

透過 Android Studio 部署

透過 Android Studio 部署時,請完成下列步驟:

  1. 開啟用戶端應用程式的 Android Studio 專案。
  2. 依序前往「Run」>「Edit Configurations」。系統隨即會顯示「Run/Debug Configuration」視窗。
  3. 在「Launch Options」下,將「Launch」設為「Specified Activity」
  4. 按一下「Activity」旁的三點圖示選單,然後為用戶端選取「Main Activity」
  5. 依序按一下「Apply」和「OK」
  6. 按一下「Run」圖示 ,在測試裝置上安裝用戶端應用程式和 SDK。

透過指令列部署

在使用指令列進行部署時,請完成下列清單中的步驟。本節假設您的 SDK 應用程式模組名稱為 sdk-app,用戶端應用程式模組的名稱為 client-app

  1. 在指令列終端機上建構 Privacy Sandbox SDK APK:

    ./gradlew :client-app:buildPrivacySandboxSdkApksForDebug
    

    這個步驟會將產生的 APK 位置輸出。這些 APK 會以本機偵錯的金鑰簽署。下一個指令將會用到這個路徑。

  2. 在裝置上安裝 APK:

    adb install -t /path/to/your/standalone.apk
    
  3. 在 Android Studio 中,依序選取「Run」>「Edit Configurations」。系統隨即會顯示「Run/Debug Configuration」視窗。

  4. 在「Install Options」下,將「Deploy」設為「Default APK」

  5. 依序按一下「Apply」和「OK」

  6. 按一下「Run」,在測試裝置上安裝 APK 套件。

對應用程式進行偵錯

如要對用戶端應用程式進行偵錯,請按一下 Android Studio 中的「Debug」按鈕

如要對 SDK 應用程式進行偵錯,請依序前往「Run」>「Attach to Process」,系統隨即顯示彈出式畫面 (如下所示)。勾選「Show all processes」方塊。在隨即顯示的清單中,尋找名為 CLIENT_APP_PROCESS_sdk_sandbox 的程序。請選取這個選項,並在 SDK 應用程式的程式碼中加入中斷點,以開始對 SDK 進行偵錯。

SDK 應用程式程序會顯示在接近對話方塊底部的清單檢視畫面中
「Choose process」畫面,可讓您選取要偵錯的 SDK 應用程式。

利用指令列啟動及停止 SDK 執行階段

如要啟動應用程式的 SDK 執行階段程序,請使用下列殼層指令:

adb shell cmd sdk_sandbox start [--user <USER_ID> | current] <CLIENT_APP_PACKAGE>

同樣地,如要停止 SDK 執行階段程序,請執行下列指令:

adb shell cmd sdk_sandbox stop [--user <USER_ID> | current] <CLIENT_APP_PACKAGE>

查看目前載入的 SDK

您可以使用 SdkSandboxManager 中的 getSandboxedSdks 功能,查看目前已載入的 SDK。

限制

如要查看目前針對 SDK 執行階段開發的功能,請參閱「版本資訊」。

程式碼範例

GitHub 上的 SDK 執行階段和隱私權保護 API 存放區包含一組可幫助您入門的 Android Studio 專案,其中包括一些展示如何初始化和呼叫 SDK 執行階段的範例。

回報錯誤和問題

您的意見回饋對 Android 版 Privacy Sandbox 至關重要!如果您發現了任何問題,或希望針對 Android 版 Privacy Sandbox 提出改進意見,請告訴我們