SDK 런타임 개발자 가이드

의견 보내기

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

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

시작하기 전에

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

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

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

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

프로젝트에 앱 모듈 1개와 SDK 모듈 1개를 추가합니다. 이렇게 하면 코드를 나란히 실행하고 테스트하기가 더 쉬워집니다. 앱 코드와 SDK 코드가 모두 분리되도록 별도의 앱을 두 개 만들어야 합니다.

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

앱을 설치하는 방법과 유사하게 Android 스튜디오나 Android 디버그 브리지(adb)를 사용하여 테스트 기기에 SDK를 설치합니다.

시작하는 데 도움이 되도록 Kotlin 및 자바 프로그래밍 언어로 샘플 앱을 만들었으며 이 GitHub 저장소에서 확인할 수 있습니다.

SDK 준비

SDK 앱의 AndroidManifest.xml 파일에서 다음 코드 스니펫과 같이 <application> 섹션에 <sdk-library><property> 요소를 포함합니다. 런타임 지원 SDK에 고유한 이름을 사용하고 버전을 제공합니다.

<application ...>
  <sdk-library android:name="com.example.privacysandbox.provider"
               android:versionMajor="1" />
  <property
        android:name="android.sdksandbox.PROPERTY_SDK_PROVIDER_CLASS_NAME"
        android:value="com.example.provider.SdkProviderImpl" />

</application>

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

initSdk()
SDK를 초기화하고 초기화가 완료되어 사용할 준비가 되면 호출 앱에 알립니다.
getView()
광고의 뷰를 만들고 설정하며 다른 Android 뷰와 동일한 방식으로 뷰를 초기화하고 원격 렌더링을 위해 뷰를 반환합니다.
onDataReceived()
sendData()를 사용하여 호스트 앱에서 전송된 모든 메타데이터를 처리합니다.

다음 코드 스니펫은 이러한 메서드를 재정의하는 방법을 보여줍니다.

Kotlin

class SdkProviderImpl : SandboxedSdkProvider() {
    override fun initSdk(
        sandboxedSdkContext: SandboxedSdkContext, params: Bundle,
        executor: Executor, initSdkCallback: InitSdkCallback
    ) {
        // Update the callback with optional data to show that initialization
        // is complete.
        executor.execute { initSdkCallback.onInitSdkFinished(Bundle()) }
    }

    override fun getView(windowContext: Context, bundle: Bundle): View {
        val webView = WebView(windowContext)
        webView.loadUrl("https://developer.android.com/privacy-sandbox")
        return webView
    }

    override fun onDataReceived(bundle: Bundle, dataReceivedCallback: DataReceivedCallback) {
        // Update the callback with optional data to show that the receiving
        // or processing of data is complete
        dataReceivedCallback.onDataReceivedSuccess(Bundle())
    }
}

자바

public class SdkProviderImpl extends SandboxedSdkProvider {
    @Override
    public void initSdk(SandboxedSdkContext sandboxedSdkContext, Bundle params,
            Executor executor, InitSdkCallback initSdkCallback) {
        // Update the callback with optional data to show that the
        // initialization is complete.
        executor.execute(() -> initSdkCallback.onInitSdkFinished(new Bundle()));
    }

    @Override
    public View getView(Context windowContext, Bundle bundle) {
        WebView webView = new WebView(windowContext);
        webView.loadUrl("https://developer.android.com/privacy-sandbox");
        return webView;
    }

    @Override
    public void onDataReceived(Bundle bundle, DataReceivedCallback callback) {
        // Update the callback with optional data to show that the receiving or
        // processing of data is complete
        dataReceivedCallback.onDataReceivedSuccess(new Bundle());
    }
}

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

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

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

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

다음 코드 스니펫은 URL에서 로드되는 간단한 동영상 뷰를 반환하는 방법을 보여줍니다.

Kotlin

class SdkProviderImpl : SandboxedSdkProvider() {

    override fun getView(windowContext: Context, params: Bundle): View {
        val videoView = VideoView(windowContext)
        videoView.setVideoURI(Uri.parse("https://test.website/video.mp4"))
        videoView.setOnPreparedListener { mp -> mp.start() }
        return videoView
    }
}

자바

public class SdkProviderImpl extends SandboxedSdkProvider {

    @Override
    public View getView(Context windowContext, Bundle params) {
        VideoView videoView = new VideoView(windowContext);
        videoView.setVideoURI(Uri.parse("https://test.website/video.mp4"));
        videoView.setOnPreparedListener(mp -> {
            mp.start();
        });
        return videoView;
    }
}

SDK에서 Storage API 사용

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

각 SDK 런타임의 별도 내부 저장소 내에서 각 SDK에는 SDK별 저장소라는 자체 저장소 디렉터리가 제공됩니다. SDK별 저장소는 SDK 런타임의 내부 저장소를 논리적으로 분리한 것으로, 각 SDK에서 사용 중인 저장용량을 확인하는 데 도움이 됩니다.

각 SDK는 SDK가 initSdk() 메서드에서 수신하는 SandboxedSdkContext 객체의 File Storage API를 사용하여 SDK별 저장소를 사용할 수 있습니다. SDK별 저장소는 클라이언트 앱이 제거될 때까지 또는 클라이언트 앱 데이터가 삭제될 때까지 유지됩니다.

SDK는 내부 저장소만 사용할 수 있습니다. 따라서 SandboxedSdkContext.getFilesDir() 또는 SandboxedSdkContext.getCacheDir()과 같은 내부 저장소 API만 작동합니다. 자세한 내용은 내부 저장소에서 액세스를 참고하세요.

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

제공된 SandboxedSdkContext를 사용하여 저장해야 합니다. 애플리케이션 컨텍스트와 같은 다른 Context 객체 인스턴스에 File Storage API를 사용하면 SDK별 내부 저장소가 활용되지 않으며 향후에 모든 상황에서의 작동이 보장되지 않습니다.

다음 코드 스니펫은 SDK 런타임에서 저장소를 사용하는 방법을 보여줍니다.

자바

public class SdkProviderImpl extends SandboxedSdkProvider {
    private SandboxedSdkContext mContext;

    @Override
    public void initSdk(SandboxedSdkContext sandboxedSdkContext, Bundle params,
            Executor executor, InitSdkCallback initSdkCallback) {
        // Store reference to the SandboxedSdkContext for later usage
        mContext = sandboxedSdkContext;
    }

    @Override
    public void onDataReceived(Bundle data, DataReceivedCallback callback) {
        // Use the SandboxedSdkContext to use storage
        final filename = "extraData";
        final String fileContents = data.getString("extraData", "");
        try (FileOutputStream fos = mContext.openFileOutput(filename, Context.MODE_PRIVATE)) {
            fos.write(fileContents.toByteArray());
            callback.onDataReceivedSuccess();
        } catch (Exception e) {
            callback.onDataReceivedError("Unable to process data.");
        }
    }
}

Kotlin

class SdkProviderImpl : SandboxedSdkProvider() {
    private lateinit var mContext: SandboxedSdkContext

    override fun initSdk(
        sandboxedSdkContext: SandboxedSdkContext, params: Bundle,
        executor: Executor, initSdkCallback: InitSdkCallback
    ) {
        // Store reference to the SandboxedContext for later usage
        mContext = sandboxedSdkContext;
    }

    override fun onDataReceived(data: Bundle, callback: DataReceivedCallback) {
        // Use the SandboxedSdkContext to use storage
        val filename = "myfile"
        val fileContents = data.getString("extraData", "")
        try {
            mContext.openFileOutput(filename, Context.MODE_PRIVATE).use {
                it.write(fileContents.toByteArray())
                callback.onDataReceivedSuccess()
        } catch (e: Exception) {
            callback.onDataReceivedError("Unable to process data.");
        }
    }
}

클라이언트 앱 업데이트

SDK 런타임에서 실행되는 SDK를 호출하려면 호출 클라이언트 앱을 다음과 같이 변경합니다.

  1. 광고가 포함된 앱의 활동에서 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"
    }

    자바

    private static final String SDK_NAME = "com.example.privacysandbox.provider";
    
    private SdkSandboxManager mSdkSandboxManager;
    private SurfaceView mClientView;
    private boolean mSdkLoaded = false;
  2. 런타임 시 SDK와 상호작용하도록 LoadSdkCallback을 구현하여 콜백 클래스를 정의합니다.

    Kotlin

    private inner class RemoteSdkCallbackImpl() : RemoteSdkCallback {
        override fun onLoadSdkSuccess(params: Bundle) {
            mSdkLoaded = true
        }
    
        override fun onLoadSdkFailure(errorCode: Int, errorMessage: String) {
            // log/show error
        }
    }
    

    자바

    private class RemoteSdkCallbackImpl implements RemoteSdkCallback {
        private RemoteSdkCallbackImpl() {}
    
        @Override
        public void onLoadSdkSuccess(Bundle params) {
            mSdkLoaded = true;
        }
    
        @Override
        public void onLoadSdkFailure(int errorCode, String errorMessage) {
            // log/show error
        }
    }
    

requestSurfacePackage()를 호출하는 동안 런타임 시 SDK에서 원격 뷰를 다시 가져오려면 콜백 클래스 RequestSurfacePackageCallback을 정의합니다.

Kotlin

private inner class RequestSurfacePackageCallbackImpl() : RequestSurfacePackageCallback {

    // Loads the remote view specified in the SDK's getView() method.
    override fun onSurfacePackageReady(
        surfacePackage: SurfacePackage,
        surfacePackageId: Int, params: Bundle
    ) {
        Handler(Looper.getMainLooper()).post {
            mClientView.setChildSurfacePackage(surfacePackage)
            mClientView.visibility = View.VISIBLE
        }
    }

    override fun onSurfacePackageError(errorCode: Int, errorMessage: String) {
        // log/show error
    }
}

자바

private class RequestSurfacePackageCallbackImpl implements RequestSurfacePackageCallback {

    // Loads the remote view specified in the SDK's getView() method.
    @Override
    public void onSurfacePackageReady(SurfacePackage surfacePackage,
            int surfacePackageId, Bundle params) {
        new Handler(Looper.getMainLooper()).post(() -> {
            mClientView.setChildSurfacePackage(surfacePackage);
            mClientView.setVisibility(View.VISIBLE);
        });
    }

    @Override
    public void onSurfacePackageError(int errorCode, String errorMessage) {
        // log/show error
    }
}

sendData()의 상태를 추적하고 SDK에서 반환된 데이터를 다시 가져오려면 콜백 클래스 SendDataCallback을 정의합니다.

Kotlin

private inner class SendDataCallbackImpl() : SendDataCallback {

    override fun onSendDataSuccess(
        params: Bundle
    ) {
        // Use the data returned by the SDK if required.
    }

    override fun onSendDataError(errorCode: Int, errorMessage: String) {
        // log/show error
    }
}

자바

private class SendDataCallbackImpl implements SendDataCallback {

    @Override
    public void onSendDataSuccess(Bundle params) {
        // Use the data returned by the SDK if required.
    }

    @Override
    public void onSendDataError(int errorCode, String errorMessage) {
        // log/show error
    }
}
  1. onCreate()에서 SdkSandboxManager와 필요한 콜백을 초기화한 후 원격 뷰를 표시하도록 요청합니다.

    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
        )
    
        val sendDataCallback = SendDataCallback()
        // Send some data to the SDK if needed
        mSdkSandboxManager.sendData(SDK_NAME, Bundle(),{ obj: Runnable -> obj.run() }, sendDataCallback)
    
        val requestSurfacePackageCallback = RequestSurfacePackageCallback()
        Handler(Looper.getMainLooper()).post {
            val bundle = Bundle()
            mSdkSandboxManager.requestSurfacePackage(
                    SDK_NAME, display!!.displayId,
                    mClientView.width, mClientView.height,
                    bundle, requestSurfacePackageCallback
            )
        }
    }
    

    자바

    @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);
    
        SendDataCallback sendDataCallback = new SendDataCallback();
        // Send some data to the SDK if needed
        mSdkSandboxManager.sendData(SDK_NAME, new Bundle(), Runnable::run, sendDataCallback);
    
        RequestSurfacePackageCallback requestSurfacePackageCallback = new RequestSurfacePackageCallback();
        new Handler(Looper.getMainLooper()).post(() -> {
            Bundle bundle = new Bundle();
            mSdkSandboxManager.requestSurfacePackage(
                    SDK_NAME, getDisplay().getDisplayId(),
                    mClientView.getWidth(), mClientView.getHeight(), bundle, requestSurfacePackageCallback);
        });
    }
    
  2. 인증서 다이제스트를 수동으로 지정합니다. 인증서 다이제스트를 찾으려면 keytool을 사용하여 디버그 키 저장소 파일에서 추출하세요. 기본 비밀번호는 android입니다.

    keytool -list -keystore ~/.android/debug.keystore
    
  3. 앱의 매니페스트 파일에 있는 <application> 요소에서 <uses-sdk-library> 요소를 추가합니다. 이 요소 내에서 android:certDigest 속성을 이전 단계의 출력으로 설정합니다.

    <application ...>
      <uses-sdk-library
          android:name="com.example.basicsandboxservice"
          android:versionMajor="1"
          android:certDigest="27:76:B1:2D:...:B1:BE:E0:28:5E" />
    </application>
    

    참고: android:certDigest에 잘못된 값을 입력하면 다음 오류가 발생합니다.

    Installation failed due to: 'Failed to commit install session \
    SESSION_ID with command cmd package install-commit SESSION_ID. \
    Error: INSTALL_FAILED_MISSING_SHARED_LIBRARY: Reconciliation \
    failed...: Reconcile failed: Package PACKAGE_NAME \
    requires differently signed sdk library; failing!'

앱 배포

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

Android 스튜디오를 통해 배포

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

  1. SDK 앱의 Android 스튜디오 프로젝트를 엽니다.
  2. Run > Edit Configurations로 이동합니다. Run/Debug Configuration 창이 표시됩니다.
  3. 시작할 활동이 없으므로 Launch Options에서 LaunchNothing으로 설정합니다.
  4. Apply를 클릭한 후 OK를 클릭합니다.
  5. Run 을 클릭하여 테스트 기기에 SDK 앱을 설치합니다.
  6. Project 도구 창에서 클라이언트 앱 모듈로 이동합니다.
  7. Run > Edit Configurations로 이동합니다. Run/Debug Configuration 창이 표시됩니다.
  8. Launch Options를 클라이언트 앱의 기본 활동으로 설정합니다.
  9. Apply를 클릭한 후 OK를 클릭합니다.
  10. Run 을 클릭하여 테스트 기기에 클라이언트 앱을 설치합니다.

명령줄에서 배포

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

  1. SDK 앱을 배포합니다.

    ./gradlew sdk-app:installDebug
    
  2. 클라이언트 앱을 배포합니다.

    ./gradlew client-app:installDebug && \
      # Start the app's activity. This example uses the sample app.
      adb shell am start -n \
      com.example.privacysandbox.client/com.example.client.MainActivity
    

앱 디버그

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

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

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

제한사항

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

코드 샘플

GitHub의 SDK 런타임 및 개인 정보 보호 API 저장소에는 SDK 런타임 초기화 및 호출 방법을 보여주는 샘플을 비롯하여 시작하는 데 도움이 되는 개별 Android 스튜디오 프로젝트 집합이 포함되어 있습니다.

버그 및 문제 신고

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