本文為第一方和第三方開發人員提供全面的指導方針,說明如何在應用程式中使用 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 的開發環境,請依照下列步驟操作:
要設定此套件,請透過以下版本提供的函式庫下載並使用 .aar 檔案:Windows 跨裝置 SDK 版本。
在 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 整合步驟
在資訊清單宣告之後,應用程式開發人員可以遵循簡單的程式碼範例,輕鬆傳送其應用程式內容。
該應用程序必須:
- 初始化/取消初始化 Continuity SDK:
- 應用程式應該判斷呼叫 Initialize 和 DeInitialize 函式的適當時間。
- 呼叫 Initialize 函式之後,應該觸發實作 IAppContextEventHandler 的回呼。
- 傳送/刪除 AppContext:
- 初始化SDK後,如果呼叫 onContextRequestReceived ,則表示連線已建立。 然後,應用程式可以將 AppContext 傳送 (包括建立和更新) 至 LTW,或從 LTW 刪除 AppContext 。
- 如果手機和電腦之間沒有連線,而應用程式將 AppContext 傳送到 LTW,應用程式會收到 onContextResponseError 並顯示「PC 未連接」的訊息。
- 當連線重新建立時, onContextRequestReceived 會再次被呼叫。 應用程式接著可以將目前的 AppContext 傳送給 LTW。
- 在 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 的整合,請遵循下列步驟:
預備
需要下列步驟來準備整合驗證:
確保已安裝專用LTW。
將 LTW 連接到您的 PC:
如需指示,請參閱 如何在電腦上管理行動裝置 。
備註
如果掃描二維碼後您沒有被重定向到 LTW,請先打開 LTW 並掃描應用程序內的二維碼。
確認合作夥伴應用程式已整合 Continuity SDK。
Validation
接下來,請依照下列步驟驗證整合:
- 啟動應用程式並初始化 SDK。 確認已呼叫 onContextRequestReceived 。
- 呼叫 onContextRequestReceived 之後,應用程式可以將 AppContext 傳送至 LTW。 如果在傳送 AppContext 之後呼叫 onContextResponseSuccess,則 SDK 整合成功。
- 如果應用程式在電腦鎖定或斷線時傳送 AppContext ,請確認 onContextResponseError 是否被呼叫為「PC not connected」。
- 連線恢復後,請再次呼叫 onContextRequestReceived ,應用程式才能將目前的 AppContext 傳送給 LTW。
下方截圖顯示電腦斷線時的日誌紀錄,並出現錯誤訊息「PC is not connected」,以及重新連線後再次呼叫 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 啟動,需要下列步驟:
首先,需要按如下方式在註冊表中新增一個項目:
[HKEY_CLASSES_ROOT\partnerapp] @="URL:PartnerApp Protocol" "URL Protocol"="" [HKEY_CLASSES_ROOT\partnerapp\shell\open\command] @="\"C:\\path\\to\\PartnerAppExecutable.exe\" \"%1\""啟動必須在 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 應用程式中處理通訊協定啟用。
首先,將協定 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>接下來,在
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。 例如,如果需要轉接通話,應用程式必須能夠從電話傳達該內容,而傳統型應用程式必須適當地瞭解該內容並繼續載入。