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

В этой статье приведены комплексные рекомендации для разработчиков первых и третьих сторон по интеграции возможностей с использованием пакета SDK Continuity в ваших приложениях. Пакет SDK для обеспечения непрерывности обеспечивает простое взаимодействие между устройствами, позволяя пользователям возобновлять действия на разных платформах, включая Android и Windows.

Следуя этому руководству, вы можете создать плавный и интегрированный пользовательский опыт на нескольких устройствах, используя XDR через Continuity SDK.

Это важно

Подключение к возобновлению работы в Windows

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

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

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

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

Предпосылки

Для приложений Android убедитесь, что перед интеграцией пакета SDK непрерывности выполняются следующие требования:

  • Минимальная версия пакета SDK: 24
  • Версия Kotlin: 1.9.x
  • Ссылка на Windows (LTW): 1.241101.XX

Для приложений Windows убедитесь, что выполнены следующие требования:

  • Минимальная версия Windows: Windows 11
  • Среда разработки: Visual Studio 2019 или более поздней версии

Замечание

В настоящее время приложения iOS не поддерживаются для интеграции с пакетом SDK для непрерывности.

Настройка среды разработки

В следующих разделах приведены пошаговые инструкции по настройке среды разработки для приложений Android и Windows.

Настройка Android

Чтобы настроить среду разработки для Android, выполните следующие действия.

  1. Чтобы настроить пакет, скачайте и используйте .aar файл через библиотеки, предоставляемые в следующих релизах: Windows Cross-Device SDK.

  2. Добавьте метатеги в файл AndroidManifest.xml приложения Android. В следующем фрагменте кода показано, как добавить необходимые метатеги:

    <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. Инициализация/деинициализация SDK континуальности:
    1. Приложение должно определить подходящее время для вызова функций Initialize и DeInitialize.
    2. После вызова функции Initialize следует активировать обратный вызов, реализующий IAppContextEventHandler.
  2. Send/Delete AppContext:
    1. После инициализации пакета SDK, если вызывается onContextRequestReceived , оно указывает, что подключение установлено. Затем приложение может отправить (включая создание и обновление) AppContext в LTW или удалить AppContext из LTW.
    2. Если между телефоном и компьютером нет подключения, и приложение отправляет AppContext в LTW, приложение получит onContextResponseError с сообщением "Компьютер не подключен."
    3. При повторной установке подключения снова вызывается onContextRequestReceived . Приложение может затем отправить текущий AppContext в LTW.
    4. После отключения onSyncServiceDisconnected или деинициализации SDK приложение не должно отправлять AppContext.

Ниже приведен пример кода. Все обязательные и необязательные поля в AppContext см. в описании AppContext.

В следующем фрагменте кода Android показано, как выполнять запросы API с помощью пакета SDK для непрерывности:

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

} 

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

Чтобы проверить интеграцию пакета SDK непрерывности в приложении, выполните следующие действия.

Подготовка

Чтобы подготовиться к проверке интеграции, необходимо выполнить следующие действия.

  1. Убедитесь, что установлена приватная LTW.

  2. Подключите LTW к компьютеру:

    Инструкции см. в статье "Управление мобильным устройством на компьютере ".

    Замечание

    Если после сканирования QR-кода вы не перенаправляетесь в LTW, сначала откройте LTW и проверьте QR-код в приложении.

  3. Убедитесь, что партнерское приложение интегрировано в пакет SDK Continuity.

Validation

Затем выполните следующие действия, чтобы проверить интеграцию:

  1. Запустите приложение и инициализируйте пакет SDK. Убедитесь, что вызывается onContextRequestReceived.
  2. После вызова onContextRequestReceived приложение может отправить AppContext в LTW. Если onContextResponseSuccess вызывается после отправки AppContext, интеграция пакета SDK выполнена успешно.
  3. Если приложение отправляет AppContext , пока компьютер заблокирован или отключен, убедитесь, что onContextResponseError вызывается с параметром "КОМПЬЮТЕР не подключен".
  4. При восстановлении подключения убедитесь, что вызывается onContextRequestReceived, и затем приложение может отправить текущий AppContext в LTW.

На снимке экрана ниже показана запись журнала при отключении компьютера с сообщением об ошибке "Компьютер не подключен" и запись журнала после повторного подключения при вызове onContextRequestReceived .

Снимок экрана записей журнала Windows, показывающий сообщение об ошибке "компьютер не подключен" и последующую запись журнала onContextRequestReceived после повторного подключения.

AppContext

XDR определяет AppContext как метаданные, с помощью которых XDR может понять, какое приложение нужно возобновить, а также контекст, с которым приложение должно быть возобновлено. Приложения могут использовать действия, чтобы пользователи могли вернуться к тому, что они делали в своем приложении, на нескольких устройствах. Действия, созданные любым мобильным приложением, отображаются на устройствах Windows пользователей до тех пор, пока эти устройства настроены для использования с узлом взаимодействия между устройствами (CDEH).  

Каждое приложение отличается, и Windows отвечает за понимание целевой программы для восстановления, а конкретные приложения в Windows должны понимать контекст. XDR предлагает универсальную схему, которая может удовлетворять требованиям для всех приложений от первого лица, а также для сценариев возобновления сторонних приложений.

contextId

  • Обязательный: Да
  • Описание. Это уникальный идентификатор, используемый для различения одного AppContext от другого. Это гарантирует, что каждый AppContext является уникально идентифицируемым.
  • Использование: обязательно создайте уникальный contextId для каждого AppContext , чтобы избежать конфликтов.

type

  • Обязательный: Да
  • Описание. Это двоичный флаг, указывающий тип AppContext , отправляемый в Link to Windows (LTW). Значение должно быть согласовано с запрошеннымContextType.
  • Использование: задайте этот флаг в соответствии с типом отправленного контекста. Например: ProtocolConstants.TYPE_RESUME_ACTIVITY.

createTime

  • Обязательный: Да
  • Описание. Эта метка времени представляет время создания AppContext.
  • Использование: запишите точное время создания AppContext .

intentUri

  • Обязательный: нет, если веб-ссылка предоставлена
  • Описание. Этот универсальный код ресурса (URI) указывает, какое приложение может продолжить передачу AppContext с исходного устройства.
  • Использование: укажите это, если вы хотите указать конкретное приложение для обработки контекста.
  • Обязательно: Нет, если предоставлен intentUri
  • Описание: Этот URI используется для запуска веб-адреса конечной точки приложения, если они выбирают не использовать магазинные приложения. Этот параметр используется только в том случае, если не указан параметр intentUri . Если оба указаны, функция intentUri будет использоваться для возобновления работы приложения в Windows.
  • Использование: только если приложение хочет возобновить работу в веб-конечных точках, а не в приложениях магазина.

идентификатор приложения (appId)

  • Обязательный: Да
  • Описание: Это имя пакета приложения, для которого предоставлен контекст.
  • Использование. Задайте для этого имя пакета приложения.

title

  • Обязательный: Да
  • Описание. Это название AppContext, например имя документа или название веб-страницы.
  • Использование: укажите понятное название, представляющее AppContext.

Предварительный просмотр

  • Обязательный: Нет
  • Описание. Это байты изображения предварительного просмотра, которые могут представлять AppContext.
  • Использование: предоставьте предварительный просмотр изображения, если оно доступно для предоставления пользователям визуального представления AppContext.

Продолжительность жизни

  • Обязательный: Нет
  • Описание: это время существования AppContext в миллисекундах. Он используется только для текущих сценариев. Если значение не задано, значение по умолчанию — 5 минут.
  • Использование. Задайте для этого значение, чтобы определить, сколько времени AppContext должно быть допустимым. Можно задать значение не более 5 минут. Любое большее значение будет автоматически сокращено до 5 минут.

URI для намерений

URI позволяет запускать другое приложение для выполнения определенной задачи, что дает возможность использования полезных сценариев взаимодействия между приложениями. Дополнительные сведения о запуске приложений с помощью URI см. в статье Запуск приложения Windows по умолчанию для URI и Create Deep Links to App Content | Разработчики Android.

Обработка ответов API в Windows

В этом разделе описывается обработка ответов API в приложениях Windows. Пакет SDK непрерывности предоставляет способ обработки ответов API для приложений Win32 и WinUI.

Пример приложения 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; 
    } 
    

Приложения WinUI

Для упакованных приложений WinUI URI протокола можно зарегистрировать в манифесте приложения проекта. Ниже показано, как обрабатывать активацию протокола в приложении WinUI.

  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> 
    

Пример WinUI 3

В следующем фрагменте кода показано, как обрабатывать активацию протокола в приложении WinUI C++ с помощью Windows App SDK:

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

С помощью веб-ссылки запустится веб-конечная точка приложения. Разработчикам приложений необходимо убедиться, что веб-ссылка, предоставленная из приложения Android, действительна, так как XDR будет использовать браузер по умолчанию системы для перенаправления на веб-ссылку, предоставленную.

Обработка аргументов, полученных из возобновления работы между устройствами

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