Partilhar via


Phone Link - Continuidade de tarefas perfeita

Os dispositivos móveis Android que instalaram o pacote "Link to Windows" podem compartilhar programaticamente tarefas recentes do seu aplicativo Android para continuar no seu PC Windows (como URLs de sites, links de documentos, faixas de música, etc.).

O Cross Device Task Continuity está evoluindo para usar o SDK de continuidade para oferecer uma integração nativa mais profunda com a Barra de Tarefas do Windows, atendendo melhor os clientes de forma natural e intuitiva. Embora a implementação original do aplicativo de continuidade de tarefas Phone Link ainda seja suportada, para novas implementações, recomendamos o uso do Cross Device Resume (XDR) na integração do SDK de continuidade para a barra de tarefas do Windows. Saiba mais: Cross Device Resume (XDR) usando o SDK de continuidade (aplicativos Android e Windows).

O SDK de continuidade permite experiências mais perfeitas entre dispositivos com o Cross Device Resume (XDR) exibindo ícones de continuação de tarefas para ajudá-lo a retomar suas tarefas recentes do dispositivo Android diretamente da barra de tarefas do Windows (sem a necessidade de depender da interface do aplicativo Phone Link).

Saiba como partilhar programaticamente tarefas recentes a partir da sua aplicação Android (como URLs de Web sites, ligações de documentos, faixas de música, etc.) para um PC Windows que tenha configurado a Ligação do Telefone. Esta funcionalidade só está disponível em dispositivos suportados para experiências de Phone Link.

Requisitos do cenário

As seguintes condições devem ser atendidas para que seu aplicativo Android acesse a continuidade da tarefa "Vincular ao Windows":

  • NÃO sincronize URLs da Web válidas para serem acessíveis pelo PC Windows
  • Sincronize links de documentos na nuvem para serem acessíveis pelo PC com Windows
  • SINCRONIZE links de documentos locais com o PC Windows que devem estar acessíveis no dispositivo móvel por meio de seu aplicativo
  • NÃO sincronize mais de 60 vezes por minuto
  • NÃO sincronize conteúdo se o usuário não estiver interagindo com sua experiência de aplicativo

O Phone Link exibirá seu conteúdo sincronizado no nó Aplicativos em "Usados recentemente" e "Sites recentes" e em um submenu de notificação.

Captura de ecrã do Phone Link de aplicações e Web sites utilizados recentemente

O Cross Device Resume (XDR) usando o SDK de continuidade (aplicativos Android e Windows) exibirá seu conteúdo sincronizado na barra de tarefas do Windows.

Captura de ecrã da Barra de Tarefas do Windows

Aprovação do recurso de acesso limitado (LAF)

A continuidade de tarefas do Phone Link é um recurso de acesso limitado (LAF). Para obter acesso, você precisará obter aprovação da Microsoft para interoperar com o pacote "Link to Windows" pré-carregado em dispositivos móveis Android.

Para solicitar acesso, envie um e-mail wincrossdeviceapi@microsoft.com com as informações listadas abaixo.

  • Descrição da sua experiência de utilizador
  • Captura de ecrã da sua aplicação onde um utilizador acede nativamente à Web ou a documentos
  • PackageId do seu aplicativo
  • Link da Google Play Store para o seu aplicativo

Se a solicitação for aprovada, você receberá instruções sobre como desbloquear o recurso. As aprovações serão baseadas em sua comunicação, desde que seu cenário atenda aos Requisitos de cenário descritos acima.

Tratamento de Dados

Ao usar a continuidade da tarefa Phone Link, a Microsoft processará e transferirá seus dados de acordo com o Contrato de Serviços Microsoft e a Declaração de Privacidade da Microsoft. Os dados transferidos para os dispositivos vinculados do usuário podem ser processados por meio dos serviços de nuvem da Microsoft para garantir a transferência confiável de dados entre dispositivos. Os dados tratados por esta API não são retidos pelos serviços de nuvem da Microsoft sujeitos ao controle do usuário final.

O SDK de continuidade que você integrará no pacote do aplicativo garante que os dados fornecidos à API sejam manipulados apenas por pacotes confiáveis da Microsoft.

Abaixo estão diretrizes gerais e exemplos de código para integração. Para obter orientações detalhadas sobre integração, consulte o documento Kotlin do SDK.

Declarações de manifesto de aplicações Android

O manifesto do aplicativo é um arquivo XML que serve como um modelo para seu aplicativo Android. O arquivo de declaração fornece informações ao sistema operacional sobre a estrutura, componentes, permissões, etc. do seu aplicativo. As declarações a seguir são necessárias para a continuidade da tarefa com "Link to Windows".

Metadados de recursos

Os aplicativos parceiros precisam primeiro registrar metadados no manifesto do aplicativo.

Para participar do contrato de contexto do aplicativo, os metadados devem ser declarados para o tipo suportado de contexto do aplicativo. Por exemplo, para adicionar metadados do provedor de contexto do aplicativo para o recurso App Handoff :

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

Se seu aplicativo oferecer suporte a mais de um tipo de contexto de aplicativo, cada tipo de metadados deverá ser adicionado. Os tipos de metadados atualmente suportados incluem:

<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" />

Para adicionar um novo tipo, o formato de nome de metadados deve ser "com.microsoft.crossdevice.xxxProvider".

Os aplicativos também devem declarar os metadados do tipo de gatilho no manifesto. Essas declarações ajudam o sistema a determinar como e quando o aplicativo deve notificar o Load-Time Weaving (LTW) sobre determinados recursos que estão ativos.

Para um gatilho de autonotificação, em que o próprio aplicativo é responsável por notificar o sistema e está habilitado em todos os dispositivos, independentemente do fabricante original do equipamento (OEM), o tipo de gatilho deve ser declarado como:

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

</application>

Para um gatilho de API do sistema, no qual o aplicativo depende de APIs do sistema para acionar o recurso "Link to Windows", habilitado apenas em dispositivos OEM específicos, o tipo de gatilho deve ser declarado como:

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

</application>

Os códigos binários dos recursos são agora:

APPLICATION_CONTEXT: 1
BROWSER_HISTORY:     2
RESUME_ACTIVITY:     4

O registro do manifesto do aplicativo pode ser semelhante a este exemplo:

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

Exemplo de código para enviar contexto de aplicativo

Depois que as declarações de manifesto do aplicativo forem adicionadas, os aplicativos parceiros "Vincular ao Windows" precisarão:

  1. Determine o tempo apropriado para chamar as funções Initialize e DeInitialize para o SDK de continuidade. Depois de chamar a função Initialize, um retorno de chamada que implementa IAppContextEventHandler deve ser acionado.

  2. Depois de inicializar o SDK de continuidade, se onContextRequestReceived() for chamado, ele indica que a conexão foi estabelecida. O aplicativo pode então enviar AppContext (incluindo criar e atualizar) para LTW ou excluir AppContext do LTW.

Certifique-se de evitar o envio de dados confidenciais no AppContext, como tokens de acesso. Além disso, se o tempo de vida for definido muito curto, o AppContext pode expirar antes de ser enviado para o PC. Recomenda-se definir uma vida útil mínima de pelo menos 5 minutos.

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

Para todos os campos obrigatórios e opcionais , consulte AppContext Description.

Descrição do AppContext

Os seguintes valores devem ser fornecidos pelos aplicativos parceiros ao enviar o contexto do aplicativo:

Chave Valor Informações adicionais
contextId [obrigatório] Usado para distingui-lo de outros contextos de aplicativo. Exclusivo para cada contexto de aplicativo. Formato: "${packageName}.${UUID.randomUUID()}"
tipo [obrigatório] Um sinalizador binário que indica qual tipo de contexto do aplicativo é enviado para LTW. O valor deve ser consistente com requestedContextType acima
createTime[obrigatório] [FR1] Carimbo de data/hora que representa a hora de criação do contexto da aplicação.
lastUpdatedTime[obrigatório] Carimbo de data/hora que representa a hora da última atualização do contexto da aplicação. Sempre que qualquer campo de contexto do aplicativo for atualizado, a hora atualizada precisará ser registrada.
teamId [opcional] Usado para identificar a organização ou grupo ao qual o aplicativo pertence.
intentUri [opcional] Usado para indicar qual aplicativo pode continuar o contexto do aplicativo entregue a partir do dispositivo de origem. O comprimento máximo é de 2083 caracteres.
appId [opcional] O pacote da aplicação a que o contexto se destina.
título[opcional] O título deste contexto de aplicativo, como um nome de documento ou título de página da Web.
link da web[opcional] O URL da página web a ser carregada num navegador para continuar o contexto da aplicação. O comprimento máximo é de 2083 caracteres.
pré-visualização[opcional] Bytes da imagem de visualização que podem representar o contexto do aplicativo
extras[opcional] Um objeto de par chave-valor contendo informações de estado específicas do aplicativo necessárias para continuar um contexto de aplicativo no dispositivo contínuo. Necessidade de fornecer quando o contexto da aplicação tiver seus dados exclusivos.
LifeTime[opcional] O tempo de vida do contexto do aplicativo em milissegundos. Usado apenas para cenário contínuo, se não estiver definido, o valor padrão é 30 dias).

Exemplo de código de continuidade do navegador

Este exemplo destaca o uso do tipo Continuidade do navegador , que difere de outros AppContext tipos.

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

    //……
}

Para todos os campos obrigatórios e opcionais , consulte Descrição do BrowserContext.

Descrição do BrowserContext

Os aplicativos parceiros podem chamar o método para adicionar o histórico do addBrowserContext navegador. Os seguintes valores devem ser fornecidos ao adicionar o histórico do navegador:

Chave Valor
browserWebUri [obrigatório] Um URI da Web que será aberto no navegador no PC (http: ou https:).
título [obrigatório] O título da página web.
carimbo de data/hora [obrigatório] A marca temporal de quando a página da Web foi aberta pela primeira vez ou refrescada pela última vez.
favIcon [opcional] O favicon da página web em bytes, deve ser pequeno em geral.

Etapas de validação de integração

  1. Prepare-se garantindo que o LTW privado esteja instalado. Confirme se o LTW está ligado ao PC: Como gerir o seu dispositivo móvel no seu PC. Confirme se o LTW está conectado ao Phone Link: Phone Link requisitos e configuração. Se depois de digitalizar o código QR, você não pode saltar para LTW, abra LTW primeiro e digitalize o código QR dentro do aplicativo. Por fim, verifique se o aplicativo parceiro integrou o SDK de continuidade.

  2. Valide iniciando o aplicativo e inicializando o SDK de continuidade. Confirme se onContextRequestReceived() foi chamado. Uma vez onContextRequestReceived() chamado, o aplicativo pode enviar o contexto do aplicativo para LTW. Se onContextResponseSuccess() for chamado após o envio do contexto do aplicativo, a integração do SDK será bem-sucedida.

Repositório entre dispositivos do Windows no GitHub

Encontre informações sobre como integrar o Windows Cross-Device SDK em seu projeto no repositório Windows-Cross-Device no GitHub.

Para obter uma lista de perguntas frequentes, consulte Perguntas frequentes sobre a ligação de telefone.