本文为第一方和第三方开发人员提供了有关如何在应用程序中使用 Continuity SDK 集成功能的综合指南。 Continuity SDK 支持无缝跨设备体验,使用户能够跨不同的平台(包括 Android 和 Windows)恢复活动。
按照本指南作,可以通过使用 Continuity SDK 利用 XDR 跨多个设备创建流畅且集成的用户体验。
重要
在 Windows 中初始化和恢复
简历是受限访问功能(LAF)。 若要获取对此 API 的访问权限,需要从Microsoft获得批准,以便与 Android 移动设备上的“链接到 Windows”包进行互作。
若要请求访问权限,请发送电子邮件 wincrossdeviceapi@microsoft.com ,其中包含下面列出的信息:
- 您的用户体验描述
- 用户在本地访问 Web 或文档的应用程序的屏幕截图
- 应用程序的 PackageId
- 应用程序的 Google Play 商店 URL
如果请求获得批准,将收到有关如何解锁该功能的说明。 只要你的情景符合情景要求,审批将视你的交流情况而定。
先决条件
对于 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 消息“电脑未连接”。
- 重新建立连接后,将再次调用 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 连接到电脑:
查看 如何在电脑上管理移动设备 的指南以获取说明。
注释
如果在扫描 QR 码后未重定向到 LTW,请先打开 LTW 并扫描应用中的 QR 码。
验证合作伙伴应用是否已集成 Continuity SDK。
Validation
接下来,按照以下步骤验证集成:
- 启动应用并初始化 SDK。 确认调用 onContextRequestReceived 。
- 调用 onContextRequestReceived 后,应用可以将 AppContext 发送到 LTW。 如果在发送 AppContext 后调用 onContextResponseSuccess,则 SDK 集成成功。
- 如果应用在电脑锁定或断开连接时发送 AppContext ,请验证 onContextResponseError 是否使用“电脑未连接”调用。
- 还原连接后,请确保再次调用 onContextRequestReceived ,然后应用可以将当前 AppContext 发送到 LTW。
下面的屏幕截图显示了当电脑断开连接时出现的错误消息“电脑未连接”,以及在重新连接后日志中对 onContextRequestReceived 再次调用的条目。
AppContext
XDR 将 AppContext 定义为元数据,通过该元数据 XDR 可以了解要恢复的应用以及必须恢复应用程序的上下文。 应用可以使用功能,使用户能够在多个设备间回到他们在应用中进行的操作。 只要这些设备已预配跨设备体验主机(CDEH),任何移动应用创建的活动才会显示在用户的 Windows 设备上。
每个应用程序都是不同的,Windows 负责理解目标应用程序的恢复状态,而 Windows 上的特定应用程序则负责理解上下文。 XDR 建议使用泛型架构,以满足所有第一方和第三方应用恢复方案的要求。
contextId
- 必需:是
- 说明:这是用于区分一个 AppContext 与另一个 AppContext 的唯一标识符。 它可确保每个 AppContext 都是唯一可识别的。
- 用法:确保为每个 AppContext 生成唯一的 contextId,以避免冲突。
类型
- 必需:是
- 说明:这是一个表示二进制标志,指示要发送到“Link to Windows (LTW)”的 AppContext 类型。 该值应与 requestedContextType 保持一致。
- 用法:根据要发送的上下文类型设置此标志。 例如,
ProtocolConstants.TYPE_RESUME_ACTIVITY。
createTime
- 必需:是
- 说明:此时间戳表示 AppContext 的创建时间。
- 用法:记录 创建 AppContext 的确切时间。
intentUri
- 必需:否,如果提供了 Web 链接
- 说明:此 URI 指示哪些应用可以继续从原始设备移交的 AppContext 。
- 用法:如果要指定特定应用来处理上下文,请提供此项。
weblink
- 必需:否,如果在提供了 intentUri 的情况下
- 说明:如果应用程序选择不使用应用商店应用,则此 URI 用于启动应用程序的 Web 终结点。 仅当未提供 intentUri 时,才使用此参数。 如果两者均已提供,intentUri将用于在 Windows 上恢复应用程序。
- 用法:仅当应用程序想要在 Web 终结点上恢复而不是存储应用程序时使用。
appId
- 必需:是
- 说明:这是上下文所针对的应用程序的包名称。
- 用法:将其设置为应用程序的包名称。
标题
- 必需:是
- 说明:这是 AppContext 的标题,例如文档名称或网页标题。
- 用法:提供表示 AppContext 的有意义的标题。
预览
- 必需:否
- 说明:这些是可以表示 AppContext 的预览图像的字节。
- 用法:提供预览图像(如果可用于为用户提供 AppContext 的可视表示形式)。
生命周期
- 必需:否
- 说明:这是毫秒的
AppContext生存期。 它仅用于正在进行的场景。 如果未设置,则默认值为 5 分钟。 - 用法:设置此项以定义
AppContext应有效的时长。 最多可以设置 5 分钟的值。 任何更大的值将自动缩短为 5 分钟。
意图 URI
URI 允许启动其他应用来执行特定任务,从而启用有用的应用间的交互方案。 有关使用 URI 启动应用的详细信息,请参阅 启动 URI 的默认 Windows 应用 并 创建指向应用内容的深层链接 |Android 开发人员。
在 Windows 中处理 API 响应
本部分介绍如何在 Windows 应用程序中处理 API 响应。 Continuity SDK 提供了一种处理 Win32、UWP 和 Windows 应用 SDK 应用的 API 响应的方法。
Win32 应用示例
对于处理协议 URI 启动的 Win32 应用,需要执行以下步骤:
首先,需要在注册表中添加一个条目,如下:
[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 可以在项目的应用清单中注册。 以下步骤演示如何在 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 示例
以下代码片段演示如何使用 Windows 应用 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
}
}
Weblink
使用 Web 链接将启动应用程序的 Web 终结点。 应用开发人员需要确保从其 Android 应用程序提供的 Web 链接有效,因为 XDR 将使用系统的默认浏览器重定向到提供的 Web 链接。
处理从跨设备恢复获取的参数
每个应用负责反序列化和解密收到的参数,并相应地处理信息,以便将正在进行的上下文从手机传输到电脑。 例如,如果需要转移呼叫,应用必须能够从手机传达该上下文,桌面应用必须正确理解该上下文并继续加载。