Телефонная связь — простая непрерывность задач

Мобильные устройства Android, устанавливающие пакет "Ссылка на Windows", могут программно предоставлять доступ к недавним задачам из приложения Android, которые будут продолжаться на компьютере с Windows (например, URL-адреса веб-сайта, ссылки на документы, музыкальные треки и т. д.).

Непрерывность задач на нескольких устройствах развивается, чтобы использовать пакет SDK непрерывности для обеспечения более глубокой интеграции с панелью задач Windows, более эффективно обслуживая клиентов естественным и интуитивно понятным способом. Хотя исходная реализация приложения непрерывности задач Phone Link по-прежнему поддерживается, для новых реализаций рекомендуется использовать перекрестное возобновление устройств (XDR) в пакете SDK непрерывности для интеграции с панелью задач Windows. Дополнительные сведения: перекрестное возобновление устройств (XDR) с помощью пакета SDK непрерывности (приложения Android и Windows).

Пакет SDK непрерывности обеспечивает более простое взаимодействие с несколькими устройствами с помощью значков продолжения задач для перекрестного устройства (XDR), которые помогут вам возобновить последние задачи устройства Android непосредственно на панели задач Windows (без необходимости полагаться на интерфейс приложения Phone Link).

Узнайте, как программно предоставлять доступ к недавним задачам из приложения Android (например, URL-адреса веб-сайта, ссылки на документы, музыкальные треки и т. д.) на компьютер с Windows, настроив телефонную ссылку. Эта функция доступна только на поддерживаемых устройствах для взаимодействия с телефонным каналом.

Требования к сценарию

Для доступа к непрерывности задач "Связь с Windows" приложения Android необходимо выполнить следующие условия:

  • Синхронизация допустимых ВЕБ-URL-адресов, доступных компьютером Windows
  • Ссылки на облачные документы do sync, доступные на компьютере с Windows
  • Do sync local document links to the Windows PC, который должен быть доступен на мобильном устройстве через приложение
  • Не синхронизировать более 60 раз в минуту
  • НЕ синхронизируйте содержимое, если пользователь не взаимодействует с вашим приложением

Ссылка на телефон будет отображать содержимое синхронизации в узле "Приложения" в разделе "Недавно использованные" и "Последние веб-сайты" и во всплывающем элементе уведомлений.

Связь с телефоном снимок экрана с недавно используемыми приложениями и веб-сайтами

Перекрестное возобновление устройств (XDR) с помощью пакета SDK для непрерывности (Приложения Android и Windows) будет отображать содержимое синхронизации на панели задач Windows.

Снимок экрана панели задач Windows

Утверждение функции ограниченного доступа (LAF)

Непрерывность задач телефонной связи — это функция ограниченного доступа (LAF). Чтобы получить доступ, необходимо получить утверждение от Корпорации Майкрософт, чтобы взаимодействовать с предварительно загруженным пакетом Link to Windows на мобильных устройствах Android.

Чтобы запросить доступ, отправьте сообщение электронной почты wincrossdeviceapi@microsoft.com с указанными ниже сведениями.

  • Описание пользовательского интерфейса
  • Снимок экрана: приложение, в котором пользователь обращается к веб-сайту или документам
  • PackageId приложения
  • Ссылка на магазин Google Play для приложения

Если запрос утвержден, вы получите инструкции по разблокировке функции. Утверждения будут основаны на ваших сообщениях, если ваш сценарий соответствует требованиям сценария , описанным выше.

Обработка данных

Используя непрерывность задач телефонной связи, корпорация Майкрософт будет обрабатывать и передавать данные в соответствии с соглашением о службах Майкрософт и заявлением о конфиденциальности Майкрософт. Данные, передаваемые на связанные устройства пользователя, могут обрабатываться через облачные службы Майкрософт, чтобы обеспечить надежную передачу данных между устройствами. Данные, обрабатываемые этим API, не сохраняются облачными службами Майкрософт, подлежащими контролю пользователей.

Пакет SDK непрерывности, который будет интегрироваться в пакет приложения, гарантирует, что данные, предоставленные API, обрабатываются только доверенными пакетами Майкрософт.

Ниже приведены общие рекомендации и примеры кода для интеграции. Подробные инструкции по интеграции см. в документе Kotlin пакета SDK.

Объявления манифеста приложения Android

Манифест приложения — это XML-файл, который служит схемой для приложения Android. Файл объявления предоставляет сведения операционной системе о структуре, компонентах, разрешениях и т. д. Следующие объявления необходимы для обеспечения непрерывности задач с помощью ссылки на Windows.

Метаданные компонентов

Партнерские приложения должны сначала зарегистрировать метаданные в манифесте приложения.

Чтобы участвовать в контракте контекста приложения, метаданные должны быть объявлены для поддерживаемого типа контекста приложения. Например, чтобы добавить метаданные поставщика контекста приложения для функции handoff приложения :

<application...>
<meta-data
android:name="com.microsoft.crossdevice.applicationContextProvider"
android:value="true" />
</application>

Если приложение поддерживает несколько типов контекста приложения, необходимо добавить каждый тип метаданных. Типы метаданных, поддерживаемые в настоящее время, включают:

<meta-data
android:name="com.microsoft.crossdevice.browserContextProvider"
android:value="true" />

<meta-data
android:name="com.microsoft.crossdevice.applicationContextProvider"
android:value="true" />

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

Чтобы добавить новый тип, формат имени метаданных должен иметь формат com.microsoft.crossdevice.xxxProvider.

Приложения также должны объявлять мета-данные типа триггера в манифесте. Эти объявления помогают системе определить, как и когда приложение должно уведомлять Load-Time Weaving (LTW) о некоторых функциях, активных.

Для триггера самостоятельного уведомления, где само приложение отвечает за уведомление системы и включается на всех устройствах, независимо от изготовителя оборудования (OEM), тип триггера должен быть объявлен следующим образом:

<application ...
<meta-data
android:name="com.microsoft.crossdevice.trigger.PartnerApp"
android:value="the sum value of all features' binary codes" />

</application>

Для триггера системного API, в котором приложение использует системные API для активации функции "Ссылка на Windows", включенной только на определенных устройствах OEM, тип триггера должен быть объявлен как:

<application ...
<meta-data
android:name="com.microsoft.crossdevice.trigger.SystemApi"
android:value="the sum value of all features' binary codes" />

</application>

Двоичные коды функций теперь:

APPLICATION_CONTEXT: 1
BROWSER_HISTORY:     2
RESUME_ACTIVITY:     4

Регистрация манифеста приложения может выглядеть следующим образом:

<?xml version="1.0" encoding="utf-8"?> 
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 

    <application … 
 
       <!-- 
           This is the meta-data represents this app supports XDR, LTW will check  
           the package before we request app context. 
       --> 
       <meta-data 
                android:name="com.microsoft.crossdevice.resumeActivityProvider" 
                android:value="true" />

             <!-- 
           This is the meta-data represents this app supports trigger from app, the
           Value is the code of XDR feature, LTW will check if the app support partner
           app trigger when receiving trigger broadcast.
           --> 
       <meta-data 
                android:name="com.microsoft.crossdevice.trigger.PartnerApp" 
                android:value="4" />

    </application>  
</manifest> 

Пример кода для отправки контекста приложения

После добавления объявлений манифеста приложения потребуется:

  1. Определите подходящее время для вызова функций Initialize и DeInitialize для пакета SDK непрерывности. После вызова функции инициализации следует активировать обратный вызов, который реализует IAppContextEventHandler .

  2. После инициализации пакета SDK непрерывности, если onContextRequestReceived() он вызывается, он указывает, что подключение установлено. Затем приложение может отправить AppContext (включая создание и обновление) в LTW или удалить AppContext из LTW.

Не забудьте избежать отправки конфиденциальных данных, AppContextнапример маркеров доступа. Кроме того, если время существования задано слишком коротко, AppContext срок действия может истекать до отправки на компьютер. Рекомендуется установить минимальное время существования не менее 5 минут.

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()}"
    }
}

Для всех обязательных и необязательных полей см. описание AppContext.

Описание AppContext

Следующие значения должны предоставляться партнерскими приложениями при отправке контекста приложения:

Ключ Значение Дополнительные сведения
contextId [обязательно] Используется для отличия от других контекстов приложения. Уникальный для каждого контекста приложения. Формат: "${packageName}.${UUID.randomUUID()}"
тип [обязательный] Двоичный флаг, указывающий, какой тип контекста приложения отправляется в LTW. Значение должно быть согласовано с запрошеннымContextType выше
createTime[обязательный] [FR1] Метка времени создания контекста приложения.
LastUpdatedTime[обязательный] Метка времени последнего обновления контекста приложения. В любое время, когда обновляются все поля контекста приложения, необходимо записать обновленное время.
teamId [необязательно] Используется для идентификации организации или группы, к которой принадлежит приложение.
intentUri [необязательно] Используется для указания того, какое приложение может продолжить контекст приложения, переданный с исходного устройства. Максимальная длина — 2083 символа.
appId [необязательно] Пакет приложения, для который используется контекст.
title[необязательно] Заголовок этого контекста приложения, например имя документа или название веб-страницы.
weblink[необязательно] URL-адрес веб-страницы для загрузки в браузере для продолжения контекста приложения. Максимальная длина — 2083 символа.
preview[необязательно] Байт изображения предварительного просмотра, который может представлять контекст приложения
extras[необязательный] Объект пары "ключ-значение", содержащий сведения о состоянии конкретного приложения, необходимые для продолжения контекста приложения на продолжающемся устройстве. Необходимо указать, когда контекст приложения имеет уникальные данные.
LifeTime[необязательно] Время существования контекста приложения в миллисекундах. Используется только для текущего сценария, если не задано, значение по умолчанию — 30 дней).

Пример кода непрерывности браузера

В этом примере выделен тип непрерывности браузера , который отличается от других 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)
    }

    //……
}

Все обязательные и необязательные поля см. в описании BrowserContext.

Описание BrowserContext

Партнерские addBrowserContext приложения могут вызывать метод для добавления журнала браузера. При добавлении журнала браузера необходимо указать следующие значения:

Ключ Значение
browserWebUri [обязательно] Веб-URI, который откроется в браузере на компьютере (http: или https:).
title [обязательный] Заголовок веб-страницы.
метка времени [обязательный] Метка времени, которую веб-страница была открыта или обновлена.
favIcon [необязательно] Favicon веб-страницы в байтах должен быть небольшим в целом.

Шаги проверки интеграции

  1. Подготовьте , убедитесь, что установлен частный LTW. Убедитесь, что LTW подключен к компьютеру: как управлять мобильным устройством на компьютере. Убедитесь, что LTW подключен к телефонной ссылке: требования к телефонной связи и настройка. Если после сканирования QR-кода вы не можете перейти в LTW, сначала откройте LTW и проверьте QR-код в приложении. Наконец, убедитесь, что партнерское приложение интегрировано с пакетом SDK непрерывности.

  2. Проверьте , запустите приложение и инициализирует пакет SDK непрерывности. Убедитесь, что onContextRequestReceived() вызывается. После onContextRequestReceived() вызова приложение может отправить контекст приложения в LTW. Если onContextResponseSuccess() после отправки контекста приложения вызывается, интеграция пакета SDK выполняется успешно.

Репозиторий Windows cross-Device на GitHub

Сведения об интеграции пакета SDK windows cross-Device в проект в репозитории Windows-Cross-Device на сайте GitHub.

Список часто задаваемых вопросов см. в Связь с телефоном часто задаваемых вопросов.