다음을 통해 공유


연속성 SDK를 사용하여 Android 및 Windows 애플리케이션용 XDR(디바이스 간 다시 시작) 구현

이 문서에서는 애플리케이션에서 Continuity SDK를 사용하여 기능을 통합하는 방법에 대한 자사 및 타사 개발자를 위한 포괄적인 지침을 제공합니다. 연속성 SDK를 사용하면 원활한 디바이스 간 환경을 사용할 수 있으므로 사용자가 Android 및 Windows를 비롯한 여러 플랫폼에서 활동을 다시 시작할 수 있습니다.

이 지침에 따라 연속성 SDK를 사용하여 XDR을 활용하여 여러 디바이스에서 원활하고 통합된 사용자 환경을 만들 수 있습니다.

중요합니다

Windows에서 기능 재개를 위한 온보딩

이력서는 제한된 접근 기능(LAF)입니다. 이 API에 대한 액세스를 얻으려면 Android 모바일 장치에서 "Windows에 연결" 프로그램과 상호 운용하기 위해 Microsoft의 허가를 받아야 합니다.

access 요청하려면 아래 나열된 정보를 wincrossdeviceapi@microsoft.com 전자 메일로 보냅니다.

  • 사용자 환경에 대한 설명
  • 사용자가 기본적으로 웹 또는 문서에 액세스하는 애플리케이션의 스크린샷
  • 애플리케이션의 PackageId
  • 애플리케이션에 대한 Google Play 스토어 URL

요청이 승인되면 기능 잠금을 해제하는 방법에 대한 지침을 받게 됩니다. 시나리오가 설명된 시나리오 요구 사항을 충족하는 경우 승인은 통신을 기반으로 합니다.

필수 조건

Android 애플리케이션의 경우 연속성 SDK를 통합하기 전에 다음 요구 사항이 충족되는지 확인합니다.

  • 최소 SDK 버전: 24
  • 코틀린 버전: 1.9.x
  • LTW (Windows에 연결): 1.241101.XX

Windows 애플리케이션의 경우 다음 요구 사항이 충족되는지 확인합니다.

  • 최소 Windows 버전: Windows 11
  • 개발 환경: Visual Studio 2019 이상

비고

iOS 애플리케이션은 현재 Continuity SDK와의 통합에 지원되지 않습니다.

개발 환경 구성

다음 섹션에서는 Android 및 Windows 애플리케이션 모두에 대한 개발 환경을 설정하기 위한 단계별 지침을 제공합니다.

Android 설정

Android용 개발 환경을 설정하려면 다음 단계를 수행합니다.

  1. 번들을 설정하려면 Windows 디바이스 간 SDK 릴리스에서 제공된 라이브러리를 통해 .aar 파일을 다운로드하여 사용하십시오.

  2. Android 애플리케이션의 AndroidManifest.xml 파일에 메타 태그를 추가합니다. 다음 코드 조각은 필요한 메타 태그를 추가하는 방법을 보여 줍니다.

    <meta-data 
        android:name="com.microsoft.crossdevice.resumeActivityProvider" 
        android:value="true" /> 
    
    <meta-data 
        android:name="com.microsoft.crossdevice.trigger.PartnerApp" 
        android:value="4" /> 
    

API 통합 단계

매니페스트 선언 후 앱 개발자는 간단한 코드 예제에 따라 앱 컨텍스트를 쉽게 보낼 수 있습니다.

앱은 다음을 수행해야 합니다.

  1. 연속성 SDK 초기화/초기화 해제:
    1. 앱은 Initialize 및 DeInitialize 함수를 호출할 적절한 시간을 결정해야 합니다.
    2. Initialize 함수를 호출한 후 IAppContextEventHandler를 구현하는 콜백이 트리거되어야 합니다.
  2. AppContext 보내기/삭제:
    1. SDK를 초기화한 후 onContextRequestReceived 가 호출되면 연결이 설정되었음을 나타냅니다. 그런 다음 앱은 AppContext 를 LTW로 보내거나 LTW에서 AppContext 를 삭제할 수 있습니다(만들기 및 업데이트 포함).
    2. 휴대폰과 PC 간에 연결이 없고 앱이 AppContext 를 LTW로 보내는 경우 앱은 "PC가 연결되지 않았습니다."라는 메시지와 함께 onContextResponseError 를 받게 됩니다.
    3. 연결이 다시 설정되면 onContextRequestReceived 가 다시 호출됩니다. 그러면 앱이 현재 AppContext를 LTW로 보낼 수 있습니다.
    4. onSyncServiceDisconnected 또는 SDK 초기화 후 앱은 AppContext를 보내지 않아야 합니다.

다음은 코드 예제입니다. AppContext의 모든 필수 및 선택적 필드에 대해서는 AppContext 설명을 참조하세요.

다음 Android 코드 조각은 Continuity SDK를 사용하여 API 요청을 만드는 방법을 보여 줍니다.

import android.os.Bundle 
import android.util.Log 
import android.widget.Button 
import android.widget.TextView 
import android.widget.Toast 
import androidx.activity.enableEdgeToEdge 
import androidx.appcompat.app.AppCompatActivity 
import androidx.core.view.ViewCompat 
import androidx.core.view.WindowInsetsCompat 
import androidx.lifecycle.LiveData 
import androidx.lifecycle.MutableLiveData 
import androidx.lifecycle.Observer 
import com.microsoft.crossdevicesdk.continuity.AppContext 
import com.microsoft.crossdevicesdk.continuity.AppContextManager 
import com.microsoft.crossdevicesdk.continuity.ContextRequestInfo 
import com.microsoft.crossdevicesdk.continuity.IAppContextEventHandler 
import com.microsoft.crossdevicesdk.continuity.IAppContextResponse 
import com.microsoft.crossdevicesdk.continuity.LogUtils 
import com.microsoft.crossdevicesdk.continuity.ProtocolConstants 
import java.util.UUID 

  

class MainActivity : AppCompatActivity() { 

    //Make buttons member variables --- 
    private lateinit var buttonSend: Button 
    private lateinit var buttonDelete: Button 
    private lateinit var buttonUpdate: Button 

    private val appContextResponse = object : IAppContextResponse { 
        override fun onContextResponseSuccess(response: AppContext) { 
            Log.d("MainActivity", "onContextResponseSuccess") 
            runOnUiThread { 
                Toast.makeText( 
                    this@MainActivity, 
                    "Context response success: ${response.contextId}", 
                    Toast.LENGTH_SHORT 
                ).show() 
            } 
        } 

        override fun onContextResponseError(response: AppContext, throwable: Throwable) { 
            Log.d("MainActivity", "onContextResponseError: ${throwable.message}") 
            runOnUiThread { 
                Toast.makeText( 
                    this@MainActivity, 
                    "Context response error: ${throwable.message}", 
                    Toast.LENGTH_SHORT 
                ).show() 

                // Check if the error message contains the specific string 
                if (throwable.message?.contains("PC is not connected") == true) { 
                    //App should stop sending intent once this callback is received 
                }
            } 
        } 
    } 

    private lateinit var appContextEventHandler: IAppContextEventHandler 

    private val _currentAppContext = MutableLiveData<AppContext?>() 

    private val currentAppContext: LiveData<AppContext?> get() = _currentAppContext 

    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        enableEdgeToEdge() 
        setContentView(R.layout.activity_main) 
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> 
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) 
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) 
            insets 
        } 

        LogUtils.setDebugMode(true) 
        var ready = false 
        buttonSend = findViewById(R.id.buttonSend) 
        buttonDelete = findViewById(R.id.buttonDelete) 
        buttonUpdate = findViewById(R.id.buttonUpdate) 
        setButtonDisabled(buttonSend) 
        setButtonDisabled(buttonDelete) 
        setButtonDisabled(buttonUpdate) 

        buttonSend.setOnClickListener { 
            if (ready) { 
                sendResumeActivity() 
            } 
        } 

        buttonDelete.setOnClickListener { 
            if (ready) { 
                deleteResumeActivity() 
            } 
        } 

        buttonUpdate.setOnClickListener { 
            if (ready) { 
                updateResumeActivity() 
            }
        } 

        appContextEventHandler = object : IAppContextEventHandler { 

            override fun onContextRequestReceived(contextRequestInfo: ContextRequestInfo) { 
                LogUtils.d("MainActivity", "onContextRequestReceived") 
                ready = true 
                setButtonEnabled(buttonSend) 
                setButtonEnabled(buttonDelete) 
                setButtonEnabled(buttonUpdate) 

            } 

  

            override fun onInvalidContextRequestReceived(throwable: Throwable) { 
                Log.d("MainActivity", "onInvalidContextRequestReceived") 

            } 

  

            override fun onSyncServiceDisconnected() { 
                Log.d("MainActivity", "onSyncServiceDisconnected") 
                ready = false 
                setButtonDisabled(buttonSend) 
                setButtonDisabled(buttonDelete) 
            } 
        } 

        // Initialize the AppContextManager 
        AppContextManager.initialize(this.applicationContext, appContextEventHandler) 

        // Update currentAppContext text view. 
        val textView = findViewById<TextView>(R.id.appContext) 

        currentAppContext.observe(this, Observer { appContext -> 
            appContext?.let { 
                textView.text = 
                    "Current app context: ${it.contextId}\n App ID: ${it.appId}\n Created: ${it.createTime}\n Updated: ${it.lastUpdatedTime}\n Type: ${it.type}" 
                Log.d("MainActivity", "Current app context: ${it.contextId}") 
            } ?: run { 
                textView.text = "No current app context available" 
                Log.d("MainActivity", "No current app context available") 
            } 
        }) 
    } 


    // Send resume activity to LTW 
    private fun sendResumeActivity() { 
        val appContext = AppContext().apply { 
            this.contextId = generateContextId() 
            this.appId = applicationContext.packageName 
            this.createTime = System.currentTimeMillis() 
            this.lastUpdatedTime = System.currentTimeMillis() 
            this.type = ProtocolConstants.TYPE_RESUME_ACTIVITY 
        } 

        _currentAppContext.value = appContext 
        AppContextManager.sendAppContext(this.applicationContext, appContext, appContextResponse) 
    } 

    // Delete resume activity from LTW 
    private fun deleteResumeActivity() { 
        currentAppContext.value?.let { 
            AppContextManager.deleteAppContext( 
                this.applicationContext, 
                it.contextId, 
                appContextResponse 
            ) 
            _currentAppContext.value = null 
        } ?: run { 
            Toast.makeText(this, "No resume activity to delete", Toast.LENGTH_SHORT).show() 
            Log.d("MainActivity", "No resume activity to delete") 
        }
    } 

    private fun updateResumeActivity() { 
        currentAppContext.value?.let { 
            it.lastUpdatedTime = System.currentTimeMillis() 
            AppContextManager.sendAppContext(this.applicationContext, it, appContextResponse) 
            _currentAppContext.postValue(it) 
        } ?: run { 
            Toast.makeText(this, "No resume activity to update", Toast.LENGTH_SHORT).show() 
            Log.d("MainActivity", "No resume activity to update") 
        } 
    } 

    private fun setButtonDisabled(button: Button) { 
        button.isEnabled = false 
        button.alpha = 0.5f 
    } 

    private fun setButtonEnabled(button: Button) { 
        button.isEnabled = true 
        button.alpha = 1.0f 
    } 

    override fun onDestroy() { 
        super.onDestroy() 
        // Deinitialize the AppContextManager 
        AppContextManager.deInitialize(this.applicationContext) 
    } 

    override fun onStart() { 
        super.onStart() 
        // AppContextManager.initialize(this.applicationContext, appContextEventHandler) 
    } 


    override fun onStop() { 
        super.onStop() 
        // AppContextManager.deInitialize(this.applicationContext) 
    } 

    private fun generateContextId(): String { 
        return "${packageName}.${UUID.randomUUID()}" 
    } 

} 

통합 유효성 검사 단계

애플리케이션에서 Continuity SDK 통합의 유효성을 검사하려면 다음 단계를 수행합니다.

준비

통합 유효성 검사를 준비하려면 다음 단계가 필요합니다.

  1. 프라이빗 LTW가 설치되어 있는지 확인합니다.

  2. PC에 LTW 연결:

    지침은 PC에서 모바일 디바이스를 관리하는 방법을 참조하세요.

    비고

    LTW로 리디렉션되지 않은 QR 코드를 검사한 후 LTW를 먼저 열고 앱 내에서 QR 코드를 검색하세요.

  3. 파트너 앱이 Continuity SDK를 통합했는지 확인합니다.

Validation

다음으로, 다음 단계에 따라 통합의 유효성을 검사합니다.

  1. 앱을 시작하고 SDK를 초기화합니다. onContextRequestReceived가 호출되어 있는지 확인합니다.
  2. onContextRequestReceived가 호출된 후 앱은 AppContext를 LTW로 보낼 수 있습니다. AppContext를 보낸 후 onContextResponseSuccess가 호출되면 SDK 통합이 성공합니다.
  3. PC가 잠겨 있거나 연결이 끊긴 동안 앱이 AppContext 를 보내는 경우 onContextResponseError 가 "PC가 연결되지 않음"으로 호출되었는지 확인합니다.
  4. 연결이 복원되면 onContextRequestReceived 가 다시 호출되고 앱이 현재 AppContext를 LTW로 보낼 수 있는지 확인합니다.

아래 스크린샷은 PC의 연결이 끊어질 때 "PC가 연결되지 않았습니다."라는 오류 메시지와 onContextRequestReceived 가 다시 호출될 때 다시 연결한 후 로그 항목이 표시된 로그 항목을 보여줍니다.

Windows 로그 항목의 스크린샷으로, PC 연결이 끊어진 오류 메시지와 재연결 후 기록된 onContextRequestReceived 로그 항목을 보여줍니다.

AppContext

XDR은 애플리케이션을 다시 시작해야 하는 컨텍스트와 함께 다시 시작할 앱을 XDR이 이해할 수 있는 메타데이터로 AppContext 를 정의합니다. 앱은 활동을 사용하여 사용자가 여러 디바이스에서 앱에서 수행한 작업으로 돌아갈 수 있도록 할 수 있습니다. 사용자의 Windows 디바이스(들)에 모든 모바일 앱에서 만들어진 활동이 나타나며, 이러한 디바이스들이 CDEH(디바이스 간 환경 호스트)로 프로비전된 경우에만 가능합니다.  

모든 애플리케이션은 서로 다르며, 애플리케이션을 다시 시작하는 것은 Windows의 책임이고, Windows의 특정 애플리케이션은 독자적으로 컨텍스트를 이해해야 합니다. XDR은 모든 자사 및 타사 앱 다시 시작 시나리오에 대한 요구 사항을 충족할 수 있는 일반 스키마를 제안합니다.

contextId

  • 필수: 예
  • 설명: 하나의 AppContext를 다른 AppContext 와 구분하는 데 사용되는 고유 식별자입니다. 이를 통해 각 AppContext 를 고유하게 식별할 수 있습니다.
  • 사용법: 충돌을 방지하려면 각 AppContext 에 대해 고유한 contextId를 생성해야 합니다.

type

  • 필수: 예
  • 설명: LTW(Windows 링크)로 전송되는 AppContext 의 형식을 나타내는 이진 플래그입니다. 값은 requestedContextType과 일치해야 합니다.
  • 사용법: 보내는 컨텍스트 유형에 따라 이 플래그를 설정합니다. 예: ProtocolConstants.TYPE_RESUME_ACTIVITY.

생성시간

  • 필수: 예
  • 설명: 이 타임스탬프는 AppContext의 생성 시간을 나타냅니다.
  • 사용법: AppContext 가 만들어진 정확한 시간을 기록합니다.

intentUri

  • 필수: 아니요, 웹 링크 가 제공된 경우
  • 설명: 이 URI는 원래 디바이스에서 전달된 AppContext 를 계속할 수 있는 앱을 나타냅니다.
  • 사용법: 컨텍스트를 처리할 특정 앱을 지정하려는 경우 이를 제공합니다.
  • 필수: 아니요, intentUri 가 제공된 경우
  • 설명: 이 URI는 스토어 앱을 사용하지 않도록 선택한 경우 애플리케이션의 웹 엔드포인트를 시작하는 데 사용됩니다. 이 매개 변수는 intentUri 가 제공되지 않은 경우에만 사용됩니다. 둘 다 제공된 경우 intentUri 를 사용하여 Windows에서 애플리케이션을 다시 시작합니다.
  • 사용: 애플리케이션이 저장소 애플리케이션이 아닌 웹 엔드포인트에서 다시 시작하려는 경우에만 사용할 수 있습니다.

appId

  • 필수: 예
  • 설명: 컨텍스트가 맞는 애플리케이션의 패키지 이름입니다.
  • 사용법: 애플리케이션의 패키지 이름으로 설정합니다.

title

  • 필수: 예
  • 설명: 문서 이름 또는 웹 페이지 제목과 같은 AppContext의 제목입니다.
  • 사용법: AppContext를 나타내는 의미 있는 제목을 제공합니다.

미리 보기

  • 필수: 아니요
  • 설명: AppContext를 나타낼 수 있는 미리 보기 이미지의 바이트입니다.
  • 사용법: 사용자에게 AppContext의 시각적 표현을 제공하는 데 사용할 수 있는 경우 미리 보기 이미지를 제공합니다.

수명

  • 필수: 아니요
  • 설명: 이 수명은 AppContext 밀리초 단위입니다. 진행 중인 시나리오에만 사용됩니다. 설정하지 않으면 기본값은 5분입니다.
  • 사용법: 유효 기간을 정의 AppContext 하도록 설정합니다. 최대 5분까지 값을 설정할 수 있습니다. 더 큰 값은 자동으로 5분으로 단축됩니다.

인텐트 URI

URI를 사용하면 다른 앱을 시작하여 특정 작업을 수행할 수 있으므로 유용한 앱 간 시나리오를 사용할 수 있습니다. URI를 사용하여 앱을 시작하는 방법에 대한 자세한 내용은 URI에 대해 기본 Windows 앱 실행앱 콘텐츠로의 딥 링크 생성 | Android 개발자를 참조하십시오.

Windows에서 API 응답 처리

이 섹션에서는 Windows 애플리케이션에서 API 응답을 처리하는 방법을 설명합니다. 연속성 SDK는 Win32 및 WinUI 앱에 대한 API 응답을 처리하는 방법을 제공합니다.

Win32 앱 예제

Win32 앱이 프로토콜 URI 시작을 처리하려면 다음 단계가 필요합니다.

  1. 먼저 다음과 같이 레지스트리에 항목을 만들어야 합니다.

    [HKEY_CLASSES_ROOT\partnerapp] 
    @="URL:PartnerApp Protocol" 
    "URL Protocol"="" 
    
    [HKEY_CLASSES_ROOT\partnerapp\shell\open\command] 
    @="\"C:\\path\\to\\PartnerAppExecutable.exe\" \"%1\"" 
    
  2. 시작은 Win32 앱의 기본 함수에서 처리해야 합니다.

    #include <windows.h> 
    #include <shellapi.h> 
    #include <string> 
    #include <iostream> 
    
    int CALLBACK wWinMain(HINSTANCE, HINSTANCE, PWSTR lpCmdLine, int) 
    { 
        // Check if there's an argument passed via lpCmdLine 
        std::wstring cmdLine(lpCmdLine); 
        std::wstring arguments; 
    
        if (!cmdLine.empty()) 
        { 
            // Check if the command-line argument starts with "partnerapp://", indicating a URI launch 
            if (cmdLine.find(L"partnerapp://") == 0) 
            { 
                // This is a URI protocol launch 
                // Process the URI as needed 
                // Example: Extract action and parameters from the URI 
                arguments = cmdLine;  // or further parse as required 
            } 
            else 
            {
                // Launched by command line or activation APIs 
            } 
        } 
        else 
        { 
            // Handle cases where no arguments were passed 
        } 
    
        return 0; 
    } 
    

WinUI 앱

패키지된 WinUI 앱의 경우 프로토콜 URI를 프로젝트의 앱 매니페스트에 등록할 수 있습니다. 다음 단계에서는 WinUI 앱에서 프로토콜 활성화를 처리하는 방법을 보여 줍니다.

  1. 먼저 프로토콜 URI는 다음과 같이 파일에 등록 Package.appxmanifest 됩니다.

    <Applications> 
            <Application Id= ... > 
                <Extensions> 
                    <uap:Extension Category="windows.protocol"> 
                      <uap:Protocol Name="alsdk"> 
                        <uap:Logo>images\icon.png</uap:Logo> 
                        <uap:DisplayName>SDK Sample URI Scheme</uap:DisplayName> 
                      </uap:Protocol> 
                    </uap:Extension> 
              </Extensions> 
              ... 
            </Application> 
       <Applications> 
    

WinUI 3 예제

다음 코드 조각은 Windows App SDK 사용하여 C++ WinUI 앱에서 프로토콜 활성화를 처리하는 방법을 보여 줍니다.

void App::OnActivated(winrt::Windows::ApplicationModel::Activation::IActivatedEventArgs const& args) 
{ 
     if (args.Kind() == winrt::Windows::ApplicationModel::Activation::ActivationKind::Protocol) 
     { 
         auto protocolArgs = args.as<winrt::Windows::ApplicationModel::Activation::ProtocolActivatedEventArgs>(); 
         auto uri = protocolArgs.Uri(); 
         std::wstring uriString = uri.AbsoluteUri().c_str(); 
         //Process the URI as per argument scheme 
     } 
} 

웹 링크를 사용하면 애플리케이션의 웹 엔드포인트가 시작됩니다. XDR은 시스템의 기본 브라우저를 사용하여 제공된 웹 링크로 리디렉션하므로 앱 개발자는 Android 애플리케이션에서 제공하는 웹 링크가 유효한지 확인해야 합니다.

장치 간 재개 기능에서 얻은 인수 처리

수신된 인수를 역직렬화 및 해독하고 그에 따라 정보를 처리하여 진행 중인 컨텍스트를 휴대폰에서 PC로 전송하는 것은 각 앱의 책임입니다. 예를 들어 통화를 전송해야 하는 경우 앱은 전화에서 해당 컨텍스트를 통신할 수 있어야 하며 데스크톱 앱은 해당 컨텍스트를 적절하게 이해하고 로드를 계속해야 합니다.