詳閱 Android 版 Privacy Sandbox 說明文件時,請利用「開發人員預覽版」或「Beta 版」按鈕選取您要使用的計畫版本,因為兩者的操作說明可能不盡相同。
SDK 執行階段可讓 SDK 在呼叫應用程式以外的專屬沙箱中執行。SDK 執行階段會針對使用者資料收集作業提供強化的保護措施和保證。這是透過改進後的執行環境來達成,這個環境會限制資料存取權和獲得授予的權限。如要進一步瞭解 SDK 執行階段,請參閱設計提案。
此頁面將逐步引導您建立啟用執行階段的 SDK,以定義可遠端轉譯至通話應用程式的網頁式檢視畫面。
已知限制
如需 SDK 執行階段的開發中功能清單,請參閱「版本資訊」。
我們預計將在下一個主要 Android 平台版本中修正下列限制。
- 在可捲動的檢視畫面中顯示廣告。舉例來說,
RecyclerView
無法正常運作。- 調整大小後可能會發生資源浪費的情形。
- 使用者觸控捲動事件不會正確傳遞至執行階段。
- Storage API
- Android 13 不支援個別 SDK 儲存空間。
下列問題將在 2023 年修正:
getAdId
和getAppSetId
API 的支援功能尚未啟用,因此這些 API 還無法正常運作。
事前準備
在開始之前,請先完成下列步驟:
為 Android 版 Privacy Sandbox 設定開發環境。支援 SDK 執行階段的工具現處於積極開發階段,因此本指南要求您使用最新的 Android Studio Canary 版本。您可以將這個版本的 Android Studio 與目前使用的其他版本同時執行,因此如果這項要求無法運作,請告訴我們。
在 Android Studio 中設定專案
如要試用 SDK 執行階段,請使用與用戶端伺服器模型類似的模型。主要差別在於應用程式 (用戶端) 和 SDK (「伺服器」) 是在同一部裝置上執行。
- 在專案中新增應用程式模組。這個模組可做為驅動 SDK 的用戶端。
- 在應用程式模組中,啟用 SDK 執行階段、宣告必要的權限,並設定 API 專屬廣告服務。
- 在專案中新增一個程式庫模組,這個模組會包含 SDK 程式碼。
- 在 SDK 模組中宣告必要權限。您不需要在這個模組中設定 API 專屬廣告服務。
- 從程式庫模組的
build.gradle
檔案中移除 SDK 未使用的dependencies
。在大多數情況下,您可以移除所有依附元件。方法是建立名稱與您的 SDK 相對應的新目錄。 使用
com.android.privacy-sandbox-sdk
類型手動建立新模組。其與 SDK 程式碼組合在一起,產生一個可以部署至您的裝置的 APK。方法是建立名稱與您的 SDK 相對應的新目錄。新增空白的build.gradle
檔案。本指南稍後會填入這個檔案的內容。在
gradle.properties
檔案中新增下列程式碼片段:android.experimental.privacysandboxsdk.enable=true
下載「Tiramisu (Extension Level 4)」模擬器映像檔,然後使用這個含有 Play 商店的映像檔建立模擬器。
視您是 SDK 開發人員還是應用程式開發人員而定,您選用的最終設定可能會與上一段所述的設定不同。
利用 Android Studio 或 Android Debug Bridge (ADB),將 SDK 安裝到測試裝置中,方法與安裝應用程式類似。為了協助您快速上手,我們以 Kotlin 和 Java 程式語言建立了一些範例應用程式,並放置於這個 GitHub 存放區。如要瞭解在 Android Studio 穩定版中執行範例所需的變更項目,請參閱 README 和資訊清單檔案的註解。
準備 SDK
手動建立模組層級目錄。這可做為實作程式碼的相關包裝函式,用來建構 SDK APK。在新目錄中新增
build.gradle
檔案,並填入下列程式碼片段。請為啟用執行階段的 SDK (RE-SDK) 設定專屬的名稱,並提供版本。將程式庫模組包含至dependencies
區段中。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>') }
在實作程式庫中建立類別,做為 SDK 的進入點。類別的名稱應對應至
sdkProviderClassName
的值,並擴充SandboxedSdkProvider
。
SDK 的進入點會擴充 SandboxedSdkProvider
。SandboxedSdkProvider
包含 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 doSomthing(); }
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. } } }
在 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
物件使用 Storage API,存取這個獨立的內部儲存空間。SDK 只能使用內部儲存空間,因此只有內部儲存 API (例如 Context.getFilesDir()
或 Context.getCacheDir()
) 才能運作。如需更多範例,請參閱「從內部儲存空間存取」一文。
無法透過 SDK 執行階段存取外部儲存空間。呼叫 API 以存取外部儲存空間時,系統會擲回例外狀況或傳回 null
。以下提供幾個範例:
- 使用儲存空間存取架構存取檔案會擲回
SecurityException
。 getExternalFilsDir()
一律會傳回null
。
在 Android 13 中,SDK 執行階段中的所有 SDK 都會共用分配給 SDK 執行階段的內部儲存空間。系統會保留儲存空間,直到用戶端應用程式解除安裝,或用戶端應用程式資料遭到清除。
您必須使用 SandboxedSdkProvider.getContext()
傳回的 Context
做為儲存空間。在任何其他 Context
物件執行個體 (例如應用程式環境) 上使用 Storage 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
存取 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,請對呼叫用戶端應用程式進行下列變更:
在應用程式資訊清單中新增
INTERNET
和ACCESS_NETWORK_STATE
權限:<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
在含有廣告的應用程式活動中,宣告
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;
檢查裝置是否提供 SDK 執行階段程序。
檢查
SdkSandboxState
常數 (getSdkSandboxState()
)。SDK_SANDBOX_STATE_ENABLED_PROCESS_ISOLATION
表示 SDK 執行階段可供使用。檢查是否成功呼叫
loadSdk()
。如果未擲回例外狀況,且接收器是SandboxedSdk
的執行個體,則表示呼叫成功。從前景呼叫
loadSdk()
。如果從背景呼叫,系統會擲回SecurityException
。檢查
OutcomeReceiver
是否有SandboxedSdk
的執行個體,以驗證是否已擲回LoadSdkException
。例外狀況表示 SDK 執行階段無法使用。
如果
SdkSandboxState
或loadSdk
呼叫失敗,則無法使用 SDK 執行階段,因此呼叫應回退至現有 SDK。實作
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()
在
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); }
如要處理 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 互動,您必須再次載入這些檢視畫面,藉此啟動新的沙箱程序。
將 SDK 模組的依附元件新增至用戶端應用程式的
build.gradle
:dependencies { ... implementation project(':<your-sdk-module>') ... }
測試應用程式
如要執行用戶端應用程式,請使用 Android Studio 或指令列在測試裝置上安裝 SDK 應用程式和用戶端應用程式。
透過 Android Studio 部署
透過 Android Studio 部署時,請完成下列步驟:
- 開啟用戶端應用程式的 Android Studio 專案。
- 依序前往「Run」>「Edit Configurations」。系統隨即會顯示「Run/Debug Configuration」視窗。
- 在「Launch Options」下,將「Launch」設為「Specified Activity」。
- 按一下「Activity」旁的三點圖示選單,然後為用戶端選取「Main Activity」。
- 依序點選「Apply」 和「OK」。
- 按一下「Run」圖示
,在測試裝置上安裝用戶端應用程式和 SDK。
透過指令列部署
在使用指令列進行部署時,請完成下列清單中的步驟。本節假設您的 SDK 應用程式模組名稱為 sdk-app
,用戶端應用程式模組的名稱為 client-app
。
在指令列終端機上建構 Privacy Sandbox SDK APK:
./gradlew :client-app:buildPrivacySandboxSdkApksForDebug
這個步驟會將產生的 APK 位置輸出。這些 APK 會以本機偵錯的金鑰簽署。下一個指令將會用到這個路徑。
在裝置上安裝 APK:
adb install -t /path/to/your/standalone.apk
在 Android Studio 中,依序選取「Run」>「Edit Configurations」。系統隨即會顯示「Run/Debug Configuration」視窗。
在「Install Options」下,將「Deploy」設為「Default APK」。
依序點選「Apply」 和「OK」。
按一下「Run」,在測試裝置上安裝 APK 套件。
對應用程式進行偵錯
如要對用戶端應用程式進行偵錯,請按一下 Android Studio 中的「Debug」 按鈕。
如要對 SDK 應用程式進行偵錯,請依序前往「Run」>「Attach to Process」,系統隨即顯示彈出式畫面 (圖 1)。勾選「Show all processes」方塊。在隨即顯示的清單中,尋找名為 CLIENT_APP_PROCESS_sdk_sandbox
的程序。請選取這個選項,並在 SDK 應用程式的程式碼中加入中斷點,以開始對 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 執行階段的開發中功能清單,請參閱「版本資訊」。
程式碼範例
GitHub 上的 SDK 執行階段和隱私權保護 API 存放區包含一組可幫助您入手的 Android Studio 專案,其中包括一些展示如何初始化和呼叫 SDK 執行階段的範例。回報錯誤和問題
您的意見回饋對 Android 版 Privacy Sandbox 至關重要!如果您發現了任何問題,或希望針對 Android 版 Privacy Sandbox 提出改進意見,請告訴我們。
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- SDK 執行階段
- 版本資訊
- Android 開發人員指南中的 Protected Audience API