共用方式為


使用 Continuity SDK 實作 Android 與 Windows 應用程式的跨裝置恢復(XDR)

本文為第一方和第三方開發人員提供全面的指導方針,說明如何在應用程式中使用 Continuity SDK 整合功能。 Continuity SDK 可實現無縫的跨裝置體驗,讓使用者能夠跨不同平台(包括 Android 和 Windows)恢復活動。

遵循本指南,您可以使用 Continuity SDK 利用 XDR,在多個裝置上建立流暢且整合的使用者體驗。

這很重要

在 Windows 中上線以繼續

履歷是一種有限存取功能(Limited Access Feature,簡稱 LAF)。 要取得此 access API,您需要取得 Microsoft 核准,才能在 Android 行動裝置上與「Link to Windows」套件互通。

如需申請access,請電郵 wincrossdeviceapi@microsoft.com,並附上以下資訊:

  • 使用者體驗的描述
  • 你的應用程式中,使用者原生存取網頁或文件時的螢幕擷取畫面
  • 您的應用程式的 PackageId
  • 應用程式的 Google Play 商店網址

如果請求獲得批准,您將收到有關如何解鎖該功能的說明。 核准將基於您的溝通,前提是您的場景符合概述的 場景要求

先決條件

對於 Android 應用程式,請確定在整合 Continuity SDK 之前符合下列需求:

  • 最低 SDK 版本:24
  • Kotlin 版本:1.9.x
  • 鏈結到 Windows (LTW):1.241101.XX

對於 Windows 應用程式,請確定符合下列需求:

  • 最低 Windows 版本:Windows 11
  • 開發環境:Visual Studio 2019 或更新版本

備註

目前不支援 iOS 應用程式與 Continuity SDK 整合。

設定您的開發環境

以下部分提供了為 Android 和 Windows 應用程式設定開發環境的逐步說明。

Android 設定

若要設定 Android 的開發環境,請依照下列步驟操作:

  1. 要設定此套件,請透過以下版本提供的函式庫下載並使用 .aar 檔案:Windows 跨裝置 SDK 版本

  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. 初始化/取消初始化 Continuity SDK:
    1. 應用程式應該判斷呼叫 Initialize 和 DeInitialize 函式的適當時間。
    2. 呼叫 Initialize 函式之後,應該觸發實作 IAppContextEventHandler 的回呼。
  2. 傳送/刪除 AppContext
    1. 初始化SDK後,如果呼叫 onContextRequestReceived ,則表示連線已建立。 然後,應用程式可以將 AppContext 傳送 (包括建立和更新) 至 LTW,或從 LTW 刪除 AppContext
    2. 如果手機和電腦之間沒有連線,而應用程式將 AppContext 傳送到 LTW,應用程式會收到 onContextResponseError 並顯示「PC 未連接」的訊息。
    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. 將 LTW 連接到您的 PC:

    如需指示,請參閱 如何在電腦上管理行動裝置

    備註

    如果掃描二維碼後您沒有被重定向到 LTW,請先打開 LTW 並掃描應用程序內的二維碼。

  3. 確認合作夥伴應用程式已整合 Continuity SDK。

Validation

接下來,請依照下列步驟驗證整合:

  1. 啟動應用程式並初始化 SDK。 確認已呼叫 onContextRequestReceived
  2. 呼叫 onContextRequestReceived 之後,應用程式可以將 AppContext 傳送至 LTW。 如果在傳送 AppContext 之後呼叫 onContextResponseSuccess,則 SDK 整合成功。
  3. 如果應用程式在電腦鎖定或斷線時傳送 AppContext ,請確認 onContextResponseError 是否被呼叫為「PC not connected」。
  4. 連線恢復後,請再次呼叫 onContextRequestReceived ,應用程式才能將目前的 AppContext 傳送給 LTW。

下方截圖顯示電腦斷線時的日誌紀錄,並出現錯誤訊息「PC is not connected」,以及重新連線後再次呼叫 onContextRequestReceived 時的日誌紀錄。

一張 Windows 日誌條目的截圖,顯示電腦未連接錯誤訊息以及重新連線後的 onContextRequestReceived 日誌條目。

應用程式上下文

XDR 將 AppContext 定義為中繼資料,XDR 可以透過中繼資料瞭解要繼續的應用程式,以及必須繼續應用程式的內容。 應用程式可以使用活動,讓使用者能夠跨多個裝置回到他們在應用程式中執行的動作。 任何行動應用程式建立的活動都會顯示在使用者的 Windows 裝置上,只要這些裝置已佈建跨裝置體驗主機 (CDEH)。  

每個應用程式都不同,需要 Windows 來了解要恢復的目標應用程式,而 Windows 上的特定應用程式則負責了解其使用情境。 XDR 提出了一種通用模式,可以滿足所有第一方和第三方應用程序恢復場景的需求。

contextId

  • 必要:是
  • 描述:這是唯一識別碼,用來區分一個 AppContext 與另一個 AppContext 。 它可確保每個 AppContext 都是唯一可識別的。
  • 用法:確保為每個 AppContext 產生唯一的contextId,以避免衝突。

型別

  • 必要:是
  • 描述:這是二進位旗標,指出要傳送至 Link to Windows (LTW) 的 AppContext 類型。 值應該與 requestedContextType 一致。
  • 用法:根據您要傳送的內容類型設定此旗標。 例如: ProtocolConstants.TYPE_RESUME_ACTIVITY

創建時間

  • 必要:是
  • 描述:此時間戳記代表 AppContext 的建立時間。
  • 使用方式:記錄建立 AppContext 的確切時間。

意圖URI

  • 必要:否,如果提供網頁連結
  • 描述:此 URI 指出哪個應用程式可以繼續從原始裝置移交的 AppContext
  • 使用方式:如果您想要指定特定應用程式來處理內容,請提供此選項。
  • 必要條件:否,如果提供 intentUri
  • 描述:如果應用程式選擇不使用市集應用程式,則此 URI 可用來啟動應用程式的 Web 端點。 只有在未提供 intentUri 時,才會使用此參數。 如果同時提供兩者,則會使用 intentUri 來在 Windows 上繼續應用程式。
  • 使用方式:僅當應用程式想要在 Web 端點恢復運行,而不是在商店應用程式上恢復時使用。

appId

  • 必要:是
  • 說明:這是內容所針對的應用程式的套件名稱。
  • 使用方式:將此設定為應用程式的套件名稱。

title

  • 必要:是
  • 描述:這是 AppContext 的標題,例如文件名稱或網頁標題。
  • 使用方式:提供代表 AppContext 的有意義的標題。

預覽

  • 必要:否
  • 描述:這些是預覽影像的位元組,可代表 AppContext
  • 使用方式:如果可用,請提供預覽影像,讓使用者以視覺化方式呈現 AppContext

壽命

  • 必要:否
  • 描述:這是 的 AppContext 生命週期,以毫秒為單位。 它僅用於進行中的場景。 如果未設定,預設值為 5 分鐘。
  • 用法:設定此選項以定義 的 AppContext 有效時間長度。 您最多可以設定 5 分鐘的值。 任何較大的值將自動縮短至 5 分鐘。

意圖 URI

URI 可讓您啟動另一個應用程式來執行特定任務,從而實現應用程式之間的有用互動場景。 欲了解更多關於使用 URI 啟動應用程式的資訊,請參閱 啟動與 URI 關聯的預設 Windows 應用程式為應用程式內容建立深層連結 | Android 開發者

在 Windows 中處理 API 回應

本節說明如何處理 Windows 應用程式中的 API 回應。 Continuity SDK 提供處理 Win32、UWP 及 Windows App SDK 應用程式的 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; 
    } 
    

UWP 應用程式

對於 UWP 應用程式,協定 URI 可以註冊在 project 的應用程式清單中。 下列步驟示範如何在 UWP 應用程式中處理通訊協定啟用。

  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> 
    
  2. 接下來,在 App.xaml.cs 檔案中,覆寫 OnActivated 方法如下:

    public partial class App 
    { 
       protected override void OnActivated(IActivatedEventArgs args) 
      { 
          if (args.Kind == ActivationKind.Protocol) 
          { 
             ProtocolActivatedEventArgs eventArgs = args as ProtocolActivatedEventArgs; 
             // TODO: Handle URI activation 
             // The received URI is eventArgs.Uri.AbsoluteUri 
          } 
       } 
    } 
    

如需在 UWP 應用程式中處理 URI 啟動的詳細資訊,請參閱 處理 URI 啟用中的步驟 3。

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

使用 Web 連結將啟動應用程式的 Web 端點。 應用程式開發人員必須確保其 Android 應用程式提供的網路連結有效,因為 XDR 會使用系統的預設瀏覽器重新導向至提供的網路連結。

處理從Cross Device Resume取得的參數

每個應用程序都有責任反序列化和解密收到的參數,並相應地處理信息,以將正在進行的上下文從手機傳輸到 PC。 例如,如果需要轉接通話,應用程式必須能夠從電話傳達該內容,而傳統型應用程式必須適當地瞭解該內容並繼續載入。