Enables Android apps to send app context to the Link to Windows (LTW) app, allowing users to resume activities on their Windows PCs.
The Continuity SDK enables Android applications to provide seamless cross-device resume capabilities between Android devices and Windows PCs. By integrating this SDK, partner apps can programmatically share recent tasks (such as website URLs, document links, or music tracks) from Android devices equipped with the "Link to Windows" package, allowing users to continue these tasks directly on their Windows PC. Learn more about seamless cross-device experiences: Phone Link - Seamless task continuity. For complete API documentation and detailed guidance, see the Continuity SDK Kotlin Docs.
The following sections will guide you through integrating our SDK.
If you need to build a local AAR package, please refer to Build local AAR.
The Continuity SDK currently supports the following features for cross-device task continuity between Android and Windows devices:
-
Cross Device Resume (XDR) — Recommended:
Share and resume recent activities (such as website URLs, document links, or music tracks) directly from your Android app to Windows PCs. These tasks can be resumed from the Windows Taskbar via deep integration with Link to Windows (LTW). -
Browser Continuity:
Send browser history from your Android app to be displayed in the Phone Link app on your PC, allowing users to conveniently open their recent web pages in their preferred browser.
Note: iOS applications are currently not supported.
Before integrating the SDK, contact wincrossdeviceapi@microsoft.com with the information listed below:
- Description of your user experience
- Screenshot of your application where a user natively accesses web or documents
- PackageId of your application
- Google Play store link for your application
Once we are officially partnered up and aligned with the content being sent across devices, the following steps need to be completed:
- Share your app's debug/release signatures to be added to the approved list
You will receive:
- A private release build of the LTW app for testing and validation
- Minimum SDK Version: 24
- Minimum Kotlin Version: 1.6.0
Add the dependency to your app's build.gradle:
dependencies {
implementation("com.microsoft.mmx:crossdevicesdk-continuity:<version>")
}Replace <version> with the latest version from the badge above.
Declare meta-data entries in your AndroidManifest.xml:
To participate in the app context contract, meta-data must be declared for the supported type of app context.
-
Cross Device Resume:
<meta-data android:name="com.microsoft.crossdevice.resumeActivityProvider" android:value="true" />
-
Browser Continuity:
<meta-data android:name="com.microsoft.crossdevice.browserContextProvider" android:value="true" />
If the app supports more than one type of app context, each type of meta-data needs to be added.
Apps need to declare the trigger type in the manifest:
-
If the feature depends on itself to notify LTW, which means it will be enabled on all the devices, the trigger type should be declared as below:
<application ... <meta-data android:name="com.microsoft.crossdevice.trigger.PartnerApp" android:value="the sum value of all features' binary codes" /> </application>
-
If the feature depends on system API trigger, which means it will only be enabled on specific OEM devices, the trigger type should be declared as below:
<application ... <meta-data android:name="com.microsoft.crossdevice.trigger.SystemApi" android:value="the sum value of all features' binary codes" /> </application>
Now all the features' binary codes are listed below:
BROWSER_HISTORY: 2
RESUME_ACTIVITY: 4
<meta-data android:name="com.microsoft.crossdevice.resumeActivityProvider" android:value="true" />
<meta-data android:name="com.microsoft.crossdevice.trigger.PartnerApp" android:value="4" />Once the app manifest declarations have been added, "Link to Windows" partner apps will need to:
-
Determine the appropriate timing to call the Initialize and DeInitialize functions for the Continuity SDK. After calling the Initialize function, a callback that implements
IAppContextEventHandlershould be triggered. -
After initializing the Continuity SDK, if
onContextRequestReceived()is called, it indicates the connection is established. The app can then sendAppContext(including create and update) to LTW or deleteAppContextfrom LTW.
val appContext = AppContext().apply {
contextId = generateContextId()
appId = packageName
createTime = System.currentTimeMillis()
lastUpdatedTime = System.currentTimeMillis()
type = ProtocolConstants.TYPE_RESUME_ACTIVITY
// set optional fields here
}
AppContextManager.sendAppContext(context, appContext, appContextResponse)Cross Device Resume (Also see the full example in Sample App)
class MainActivity : AppCompatActivity() {
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()
}
}
}
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
val buttonSend: Button = findViewById(R.id.buttonSend)
val buttonDelete: Button = findViewById(R.id.buttonDelete)
val buttonUpdate: Button = findViewById(R.id.buttonUpdate)
setButtonDisabled(buttonSend)
setButtonDisabled(buttonDelete)
setButtonDisabled(buttonUpdate)
buttonSend.setOnClickListener {
if (ready) {
sendAppContext()
}
}
buttonDelete.setOnClickListener {
if (ready) {
deleteAppContext()
}
}
buttonUpdate.setOnClickListener {
if (ready) {
updateAppContext()
}
}
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 app context to LTW
private fun sendAppContext() {
val appContext = AppContext().apply {
this.contextId = generateContextId()
this.appId = applicationContext.packageName
this.createTime = System.currentTimeMillis()
this.lastUpdatedTime = System.currentTimeMillis()
// Set the type of app context, for example, resume activity.
this.type = ProtocolConstants.TYPE_RESUME_ACTIVITY
// Set the rest fields in appContext
//……
}
_currentAppContext.value = appContext
AppContextManager.sendAppContext(this.applicationContext, appContext, appContextResponse)
}
// Delete app context from LTW
private fun deleteAppContext() {
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")
}
}
// Update app context from LTW
private fun updateAppContext() {
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)
}
private fun generateContextId(): String {
return "${packageName}.${UUID.randomUUID()}"
}
}For all the required and optional fields, see AppContext
class MainActivity : AppCompatActivity() {
private val appContextResponse = object : IAppContextResponse {
override fun onContextResponseSuccess(response: AppContext) {
Log.d("MainActivity", "onContextResponseSuccess")
}
override fun onContextResponseError(response: AppContext, throwable: Throwable) {
Log.d("MainActivity", "onContextResponseError: ${throwable.message}")
}
}
private lateinit var appContextEventHandler: IAppContextEventHandler
private val browserHistoryContext: BrowserHistoryContext = BrowserHistoryContext()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//……
LogUtils.setDebugMode(true)
var ready = false
val buttonSend: Button = findViewById(R.id.buttonSend)
val buttonDelete: Button = findViewById(R.id.buttonDelete)
setButtonDisabled(buttonSend)
setButtonDisabled(buttonDelete)
buttonSend.setOnClickListener {
if (ready) {
sendBrowserHistory ()
}
}
buttonDelete.setOnClickListener {
if (ready) {
clearBrowserHistory ()
}
}
appContextEventHandler = object : IAppContextEventHandler {
override fun onContextRequestReceived(contextRequestInfo: ContextRequestInfo) {
LogUtils.d("MainActivity", "onContextRequestReceived")
ready = true
setButtonEnabled(buttonSend)
setButtonEnabled(buttonDelete)
}
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)
}
// Send browser history to LTW
private fun sendBrowserHistory () {
browserHistoryContext.setAppId(this.packageName)
browserHistoryContext.addBrowserContext(System.currentTimeMillis(),
Uri.parse("https://www.bing.com/"), "Bing Search", null
)
AppContextManager.sendAppContext(this.applicationContext, browserHistoryContext, appContextResponse)
}
// Clear browser history from LTW
private fun clearBrowserHistory() {
browserHistoryContext.setAppId(this.packageName)
browserHistoryContext.setBrowserContextEmptyFlag(true)
AppContextManager.sendAppContext(this.applicationContext, browserHistoryContext, appContextResponse)
}
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)
}
//……
}For all the required and optional fields, see BrowserContext
-
Ensure private LTW is installed.
-
Ensure LTW is connected to PC and Phone Link: a. Connect LTW to PC: refer to How to manage your mobile device on your PC for instructions. b. Connect LTW to Phone Link: refer to ‘How to link your devices starting from Phone Link’ section in Phone Link requirements and setup - Microsoft Support. Note: (If after scanning the QR code you cannot jump into LTW, please open LTW first and scan the QR code within the app.)
-
Verify that the partner app has integrated the Continuity SDK.
- Launch the app and initialize the SDK. Confirm that
onContextRequestReceived()is called. - Once
onContextRequestReceived()is called, the app can send the app context to LTW. IfonContextResponseSuccess()is called after sending app context, the SDK integration is successful.
Notes
- Avoid sending sensitive data (e.g., tokens) in the context
- The required/optional keys shown in the table below are for general reference only. The actual required fields may vary depending on the feature (e.g., Cross Device Resume).
Please contact us first. The final required keys will be determined based on our communication.
| Key | Value | Extra Information |
|---|---|---|
contextId[required] |
Used to distinguish it from other app contexts. | Unique for each app context. Format: ${packageName}.${UUID.randomUUID()} |
type[required] |
A binary flag that indicates what app context type is sent to LTW. | The value should be consistent with requestedContextType above |
createTime[required] |
Unix timestamp in milliseconds representing the create time of the app context. | Suggest using System.currentTimeMillis(). |
lastUpdatedTime[required] |
Unix timestamp in milliseconds representing the last updated time of the app context. | Suggest using System.currentTimeMillis(). Any time when any fields of app context is updated, the updated time needs to be recorded. |
teamId[optional] |
Used to identify the organization or group the app belongs to. | |
intentUri[optional] |
Used to indicate which app can continue the app context handed over from the originating device. | The maximum length is 2083 characters. |
appId[optional] |
The package of the application the context is for. | |
title[optional] |
The title of this app context, such as a document name or web page title. | |
weblink[optional] |
The URL of the webpage to load in a browser to continue the app context. | The maximum length is 2083 characters. |
preview[optional] |
Bytes of the preview image that can represent the app context | |
extras[optional] |
A key-value pair object containing app-specific state information needed to continue an app context on the continuing device. | Need to provide when the app context has its unique data. |
LifeTime[optional] |
The lifetime of the app context in milliseconds. | Only used for ongoing scenario, if not set, the default value is 30 days. For XDR: the maximum supported value is 5 minutes. |
| Key | Value |
|---|---|
browserWebUri[required] |
A web URI that will open in browser on PC (http: or https:). |
title[required] |
The title of the web page. |
timestamp[required] |
The timestamp that the web page was first opened or last refreshed. |
favIcon[optional] |
The favicon of the web page in bytes, should be small in general. |
Clone the repo and run assemble to generate the local AAR, or run partnerapptriggertestapp to install the test app.