SDK 런타임 개발자 가이드

안내가 다를 수 있으므로 Android의 개인 정보 보호 샌드박스 문서를 읽으면서 개발자 프리뷰 또는 베타 버튼을 사용하여 작업 중인 프로그램 버전을 선택하세요.


의견 보내기

SDK 런타임을 사용하면 SDK가 호출 앱과는 별도인 전용 샌드박스에서 실행될 수 있습니다. SDK 런타임은 사용자 데이터 수집에 관한 향상된 보호 장치와 보장을 제공합니다. 이는 데이터 액세스 권한과 허용된 권한 집합을 제한하는 수정된 실행 환경을 통해 이루어집니다. SDK 런타임에 관한 자세한 내용은 설계 제안을 참고하세요.

이 페이지의 단계는 호출 앱에 원격으로 렌더링될 수 있는 웹 기반 뷰를 정의하는 런타임 지원 SDK를 만드는 프로세스를 안내합니다.

알려진 제한사항

SDK 런타임의 진행 중인 기능 목록은 출시 노트를 참고하세요.

다음 제한사항은 다음 메이저 Android 플랫폼 버전에서 수정될 예정입니다.

  • 스크롤 가능한 뷰 내 광고 렌더링. 예를 들어 RecyclerView가 제대로 작동하지 않습니다.
    • 크기 조절 시 버벅거림이 발생할 수 있습니다.
    • 사용자 터치 스크롤 이벤트가 런타임에 제대로 전달되지 않습니다.
  • Storage API

다음 문제는 2023년에 수정될 예정입니다.

  • getAdIdgetAppSetId API를 위한 지원이 아직 활성화되지 않았으므로 이 두 API가 아직 제대로 작동하지 않습니다.

시작하기 전에

시작하기 전에 다음 단계를 완료하세요.

  1. Android의 개인 정보 보호 샌드박스의 개발 환경을 설정합니다. 도구를 사용하여 SDK 런타임을 지원하는 기능은 현재 개발 중이므로 이 가이드에서는 최신 Android 스튜디오 Canary 버전을 사용해야 합니다. 이 버전의 Android 스튜디오는 사용 중인 다른 버전과 동시에 실행할 수 있으므로 이 요구사항이 적합하지 않은 경우 Google에 알려주세요.

  2. 지원되는 기기에 시스템 이미지를 설치하거나 Android의 개인 정보 보호 샌드박스 지원이 포함된 에뮬레이터를 설정합니다.

Android 스튜디오에서 프로젝트 설정

SDK 런타임을 사용해 보려면 클라이언트-서버 모델과 유사한 모델을 사용하세요. 주요 차이점은 앱(클라이언트)과 SDK('서버')가 동일한 기기에서 실행된다는 점입니다.

  1. 프로젝트에 앱 모듈을 추가합니다. 이 모듈은 SDK를 구동하는 클라이언트 역할을 합니다.
  2. 앱 모듈에서 SDK 런타임을 사용 설정하고 필요한 권한을 선언하고 API별 광고 서비스를 구성합니다.
  3. 프로젝트에 라이브러리 모듈 1개를 추가합니다. 이 모듈에는 SDK 코드가 포함됩니다.
  4. SDK 모듈에서 필요한 권한을 선언합니다. 이 모듈에서는 API별 광고 서비스를 구성할 필요가 없습니다.
  5. SDK가 사용하지 않는 라이브러리 모듈의 build.gradle 파일에서 dependencies를 삭제합니다. 대부분의 경우 종속 항목을 모두 삭제할 수 있습니다. 그렇게 하려면 SDK에 해당하는 이름의 새 디렉터리를 만들면 됩니다.
  6. com.android.privacy-sandbox-sdk 유형을 사용하여 직접 새 모듈을 만듭니다. 이는 기기에 배포할 수 있는 APK를 만들기 위해 SDK 코드와 함께 번들로 제공됩니다. 그렇게 하려면 SDK에 해당하는 이름의 새 디렉터리를 만들면 됩니다. 빈 build.gradle 파일을 추가합니다. 이 파일의 콘텐츠는 이 가이드의 뒷부분에서 채워집니다.

  7. gradle.properties 파일에 다음 스니펫을 추가합니다.

    android.experimental.privacysandboxsdk.enable=true
    
  8. TiramisuPrivacySandbox 에뮬레이터 이미지를 다운로드하고 Play 스토어가 포함된 이 이미지로 에뮬레이터를 만듭니다.

SDK 개발자인지 앱 개발자인지에 따라 최종 설정이 이전 단락에서 설명한 것과 다를 수 있습니다.

앱을 설치하는 방법과 유사하게 Android 스튜디오나 Android 디버그 브리지(adb)를 사용하여 테스트 기기에 SDK를 설치합니다. 시작하는 데 도움이 되도록 Kotlin 및 자바 프로그래밍 언어로 샘플 앱을 만들었으며 이 GitHub 저장소에서 확인할 수 있습니다. 리드미 및 매니페스트 파일에는 안정화 버전의 Android 스튜디오에서 샘플을 실행하려면 무엇을 변경해야 하는지 설명하는 주석이 있습니다.

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의 진입점은 SandboxedSdkProvider를 확장합니다. SandboxedSdkProvider에는 SDK의 Context 객체가 포함되어 있으며 getContext()를 호출하여 이 객체에 액세스할 수 있습니다. onLoadSdk()가 호출된 후에만 이 컨텍스트에 액세스해야 합니다.

SDK 앱을 컴파일하려면 SDK 수명 주기를 처리하는 메서드를 재정의해야 합니다.

onLoadSdk()

샌드박스에서 SDK를 로드하고 SDK가 새 SandboxedSdk 객체 내에 래핑된 IBinder 객체로 인터페이스를 전달하여 요청을 처리할 준비가 되면 호출 앱에 알립니다. 바인드된 서비스 가이드IBinder를 제공하는 다양한 방법을 제공합니다. 원하는 방식을 선택할 수 있지만 SDK와 호출 앱에 일관되어야 합니다.

예를 들어 AIDL을 사용하면 앱에서 공유하고 사용할 IBinder를 표시하도록 AIDL 파일을 정의해야 합니다.

// 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에서 사용할 수 있는 컨텍스트 기반 시스템 서비스 래퍼입니다. SandboxedSdkProvider#getContext()에서 수신한 컨텍스트를 사용하고 이 컨텍스트에서 context.getSystemService(SdkSandboxController.class)를 호출하여 SDK에서 가져올 수 있습니다. 컨트롤러에는 SDK가 개인 정보 보호 샌드박스에서 정보를 가져오고 상호작용하는 데 도움이 되는 API가 있습니다.

컨트롤러에 있는 대부분의 API는 SandboxedSdkContext가 액세스에 사용되는 컨텍스트가 아니면 예외를 발생시킵니다. Google에서는 다른 컨텍스트에서 서비스 래퍼를 사용할 수 없도록 할 계획입니다. SdkSandboxController는 SDK 제공업체에서 사용하도록 고안되었으며 앱 개발자는 사용하지 않는 것이 좋습니다.

SDK 간 통신

런타임의 SDK는 서로 통신하여 미디에이션 및 관련 사용 사례를 지원할 수 있어야 합니다. 여기에 설명된 도구 모음은 샌드박스 내의 다른 SDK에서 사용할 수 있는 SDK와 작동하는 인터페이스를 제공합니다. 이 SDK 런타임 구현은 SDK 간 통신을 설정하기 위한 첫 번째 단계이며, 아직 개인 정보 보호 샌드박스에서의 미디에이션을 위한 모든 사용 사례를 포함하지 않을 수 있습니다.

SdkSandboxControllergetSandboxedSdks() API는 개인 정보 보호 샌드박스의 모든 로드된 SDK에 SandboxedSdk 클래스를 제공합니다. SandboxedSdk 객체에는 SDK에 관한 세부정보와 클라이언트가 SDK와 통신할 수 있는 sdkInterface가 포함되어 있습니다.

개인 정보 보호 샌드박스의 SDK는 다음과 같은 코드 스니펫을 사용하여 다른 SDK와 통신해야 합니다. 'SDK1'이 'SDK2'와 통신하기 위해 이 코드를 작성했다고 가정해 보겠습니다(둘 다 개인 정보 보호 샌드박스의 앱에서 로드함).

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에서 생성된 바인더 코드가 포함되어 있습니다. 두 SDK 모두 이 AIDL 라이브러리를 내보내야 합니다. 이는 앱이 개인 정보 보호 샌드박스의 SDK와 통신할 때 예상되는 작업과 동일합니다.

SDK 간 인터페이스를 공유하는 자동 생성 방식에 대한 지원이 향후 업데이트에 추가될 예정입니다.

런타임의 SDK는 아직 런타임이 지원되지 않는 앱 종속 항목 및 광고 SDK와 통신해야 할 수 있습니다.

SdkSandboxManagerregisterAppOwnedSdkSandboxInterface() API는 런타임이 지원되지 않는 SDK가 인터페이스를 플랫폼에 등록할 수 있는 방법을 제공합니다. SdkSandboxControllergetAppOwnedSdkSandboxInterfaces() 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는 활동 태그를 매니페스트 파일에 추가할 수 없으며 자체 활동을 직접 시작할 수 없습니다. 액세스는 SdkSandboxActivityHandler를 등록하고 샌드박스 활동을 시작하여 Activity 객체에 제공됩니다.

1. SdkSandboxActivityHandler 등록

SdkSandboxController#registerSdkSandboxActivityHandler(SandboxedActivityHandler)를 사용하여 SdkSandboxActivityHandler 인스턴스 등록

API는 이 객체를 등록하고, 전달된 SdkSandboxActivityHandler를 식별하는 IBinder 객체를 반환합니다.

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

API는 이 객체를 등록하고, 전달된 SdkSandboxActivityHandler를 식별하는 IBinder 객체를 반환합니다.

2. 샌드박스 활동 시작

SDK는 등록된 SdkSandboxActivityHandler를 식별하는 반환된 토큰을 클라이언트 앱에 전달합니다. 그러면 클라이언트 앱은 SdkSandboxManager#startSdkSandboxActivity(Activity, Binder)를 호출하여 샌드박스를 시작할 활동과 등록된 SdkSandboxActivityHandler를 식별하는 토큰을 전달합니다.

이 단계에서는 요청하는 SDK와 동일한 SDK 런타임에서 실행되는 새 플랫폼 활동이 시작됩니다.

활동이 시작되면 SDK는 Activity#OnCreate(Bundle) 실행의 일부로 SdkSandboxActivityHandler#onActivityCreated(Activity) 호출을 통해 알림을 받습니다.

예를 들어 Activity 객체에 액세스할 수 있으므로 호출자는 Activity#setContentView(View)를 호출하여 contentView 뷰를 설정할 수 있습니다.

수명 주기 콜백을 등록하려면 Activity#registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks)를 사용하세요.

전달된 활동에 OnBackInvokedCallback을 등록하려면 Activity#getOnBackInvokedDispatcher().registerOnBackInvokedCallback(Int, OnBackInvokedCallback)을 사용합니다.

SDK 런타임에서 동영상 플레이어 테스트

개인 정보 보호 샌드박스는 배너 광고뿐만 아니라 SDK 런타임 내에서 실행되는 동영상 플레이어도 지원하기 위해 노력하고 있습니다.

동영상 플레이어를 테스트하는 흐름은 배너 광고 테스트와 유사합니다. SDK 진입점의 getView() 메서드를 변경하여 반환된 View 객체에 동영상 플레이어를 포함합니다. 개인 정보 보호 샌드박스에서 지원할 것으로 예상되는 모든 동영상 플레이어 흐름을 테스트합니다. 동영상 수명 주기와 관련된 SDK와 클라이언트 앱 간의 통신은 범위를 벗어나므로 이 기능에 관한 의견은 아직 필요하지 않습니다.

테스트와 의견을 통해 SDK 런타임이 원하는 동영상 플레이어의 모든 사용 사례를 지원하도록 할 수 있습니다.

다음 코드 스니펫은 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;
        }
    }

SDK에서 Storage API 사용

SDK 런타임에서 SDK는 더 이상 앱의 내부 저장소에 액세스하거나 이를 읽거나 쓸 수 없으며, 그 반대도 불가능합니다. SDK 런타임은 앱과 분리되도록 보장된 자체 내부 저장소 영역에 할당됩니다.

SDK는 SandboxedSdkProvider#getContext()에서 반환하는 Context 객체의 File Storage API를 사용하여 별도의 이 내부 저장소에 액세스할 수 있습니다. SDK는 내부 저장소만 사용할 수 있으므로 Context.getFilesDir() 또는 Context.getCacheDir() 같은 Internal Storage API만 작동합니다. 더 많은 예는 내부 저장소에서 액세스를 참고하세요.

SDK 런타임에서는 외부 저장소에 액세스할 수 없습니다. 외부 저장소에 액세스하기 위해 API를 호출하면 예외가 발생하거나 null이 반환됩니다. 몇 가지 예는 다음과 같습니다.

Android 13에서는 SDK 런타임의 모든 SDK가 SDK 런타임에 할당된 내부 저장소를 공유합니다. 저장소는 클라이언트 앱이 제거될 때까지 또는 클라이언트 앱 데이터가 삭제될 때까지 유지됩니다.

SandboxedSdkProvider.getContext()에서 반환된 Context를 사용하여 저장해야 합니다. 애플리케이션 컨텍스트와 같은 다른 Context 객체 인스턴스에 File 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

클라이언트의 SharedPreferences 읽어오기

클라이언트 앱에서는 SharedPreferences의 키 집합을 SdkSandbox와 공유하도록 선택할 수 있습니다. 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 런타임을 사용하지 못할 수도 있다는 의미입니다.

    SdkSandboxState 또는 loadSdk 호출이 실패하면 SDK 런타임을 사용할 수 없으며, 호출이 기존 SDK로 대체됩니다.

  4. 로드된 후 런타임 시 SDK와 상호작용하도록 OutcomeReceiver를 구현하여 콜백 클래스를 정의합니다. 다음 예에서는 클라이언트가 콜백을 사용하여 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.
            }
        }
    

    requestSurfacePackage()를 호출하는 동안 런타임 시 SDK에서 원격 뷰를 다시 가져오려면 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와 직접 통신하세요. 앱에서 동의 관리 플랫폼(CMP) SDK를 사용할 경우 샌드박스 내부의 SDK가 기본 SharedPreferences의 데이터 CMP SDK 저장소를 읽어오고자 할 때 이러한 API를 사용할 수도 있습니다.

    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. 클라이언트 앱의 build.gradle에 SDK 모듈의 종속 항목을 추가합니다.

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

앱 테스트

클라이언트 앱을 실행하려면 Android 스튜디오 또는 명령줄을 사용하여 SDK 앱과 클라이언트 앱을 테스트 기기에 설치하세요.

Android 스튜디오를 통해 배포

Android 스튜디오를 통해 배포할 때는 다음 단계를 완료하세요.

  1. 클라이언트 앱의 Android 스튜디오 프로젝트를 엽니다.
  2. Run > Edit Configurations로 이동합니다. Run/Debug Configuration 창이 표시됩니다.
  3. Launch Options에서 LaunchSpecated Activity로 설정합니다.
  4. Activity 옆에 있는 점 3개로 된 메뉴를 클릭하고 클라이언트의 Main Activity을 선택합니다.
  5. Apply를 클릭한 후 OK를 클릭합니다.
  6. Run 을 클릭하여 테스트 기기에 클라이언트 앱과 SDK를 설치합니다.

명령줄에서 배포

명령줄을 사용하여 배포할 때는 다음 목록의 단계를 완료하세요. 이 섹션에서는 SDK 앱 모듈의 이름이 sdk-app이고 클라이언트 앱 모듈의 이름이 client-app이라고 가정합니다.

  1. 명령줄 터미널에서 개인 정보 보호 샌드박스 SDK APK를 빌드합니다.

    ./gradlew :client-app:buildPrivacySandboxSdkApksForDebug
    

    이렇게 하면 생성된 APK의 위치가 출력됩니다. 이러한 APK는 로컬 디버그 키로 서명됩니다. 다음 명령어에 이 경로가 필요합니다.

  2. 기기에 APK를 설치합니다.

    adb install -t /path/to/your/standalone.apk
    
  3. Android 스튜디오에서 Run > Edit Configurations를 클릭합니다. Run/Debug Configuration 창이 표시됩니다.

  4. Installation Options에서 DeployDefault APK로 설정합니다.

  5. Apply를 클릭한 후 OK를 클릭합니다.

  6. Run을 클릭하여 테스트 기기에 APK 번들을 설치합니다.

앱 디버그

클라이언트 앱을 디버그하려면 Android 스튜디오에서 Debug 버튼을 클릭합니다.

SDK 앱을 디버그하려면 Run > Attach to Process로 이동합니다. 그러면 팝업 화면 (아래 참고)이 표시됩니다. Show all processes 체크박스를 선택합니다. 표시되는 목록에서 CLIENT_APP_PROCESS_sdk_sandbox라는 프로세스를 찾습니다. 이 옵션을 선택하고 SDK 앱의 코드에 중단점을 추가하여 SDK 디버깅을 시작합니다.

SDK 앱 프로세스가 대화상자의 하단에 있는 목록 보기에 표시됩니다.
디버깅할 SDK 앱을 선택할 수 있는 Choose process 화면

명령줄에서 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 저장소에는 SDK 런타임 초기화 및 호출 방법을 보여주는 샘플을 비롯하여 시작하는 데 도움이 되는 개별 Android 스튜디오 프로젝트 집합이 포함되어 있습니다.

버그 및 문제 신고

여러분의 의견은 Android의 개인 정보 보호 샌드박스에서 매우 중요한 부분입니다. 발견한 문제나 Android의 개인 정보 보호 샌드박스 개선을 위한 아이디어가 있다면 Google에 알려 주세요.