Engenheiro de relações com desenvolvedores
Como os usuários alternam entre smartphones, wearables e periféricos de áudio, oferecer experiências fluidas em vários dispositivos se tornou essencial para o engajamento. No entanto, as abordagens de rede padrão geralmente resultam em consumo de bateria, picos de latência e sessões de usuário fragmentadas.
Esta investigação apresenta APIs especializadas do Android criadas para comunicação multidispositivo, oferecendo padrões eficientes de memória e energia para manter as transições entre dispositivos instantâneas, confiáveis e intuitivas.
Transcrição
Olá!
Este é o curso "Como permitir interações perfeitas entre dispositivos".
Meu nome é Hanrui, e hoje estou com meus colegas, Yacine e Nataraj.
Quando falamos de conectividade, o que costuma vir à mente é um link de rede básico ou fones de ouvido Bluetooth.
No Android, a conectividade é muito mais ampla do que isso.
O objetivo é fazer com que os dispositivos funcionem melhor em conjunto.
Hoje, vamos analisar cenários comuns de vários dispositivos e mostrar qual API é adequada para cada um deles.
Por muito tempo, criamos apps para um único dispositivo, apenas smartphones.
Depois, os wearables decolaram.
Logo, tornou-se muito comum ter um smartwatch pareado com um smartphone, sincronizando dados em segundo plano ou recebendo chamadas diretamente.
Adicionar fones de ouvido sem fio.
Agora, os usuários podem trocar de dispositivo de áudio durante uma chamada, do relógio para o smartphone, para os fones de ouvido ou para o carro.
Mais recentemente, muitos usuários também carregam um segundo dispositivo de tela grande, como um dobrável ou tablet.
Eles esperam que o app funcione sem problemas em todos esses dispositivos.
Isso se estende a laptops e Chromebooks, em que os usuários podem executar apps Android e ficar conectados diretamente da mesa de trabalho.
Em 2023, um usuário médio já tinha entre três e quatro dispositivos.
Os usuários esperam mais do que apenas uma funcionalidade independente nesses dispositivos.
Eles buscam uma experiência extraordinária de integração.
Essa experiência de integração fica ainda mais evidente quando as pessoas estão no mesmo ambiente, como nós hoje, e querem uma maneira fácil de compartilhar fotos, links ou apps com quem está por perto.
Analisando os números, o ecossistema é enorme e está crescendo rapidamente.
Neste ano, há cerca de 22 bilhões de dispositivos de IoT no mundo todo, um crescimento de 14% ano a ano.
Entre todos esses endpoints, o Wi-Fi representa um terço e o Bluetooth, um quarto.
A conectividade multidispositivo não é mais um caso extremo.
Ela faz parte do nosso dia a dia.
No entanto, hoje em dia, a maioria dos apps ainda cria recursos multidispositivo em APIs de rede e Bluetooth de baixo nível, o que gera três desafios.
Primeiro, bateria e memória.
Manter verificações e serviços em segundo plano em execução consome bateria e memória.
Segundo, latência.
Atrasos imprevisíveis interrompem chamadas em tempo real e a sincronização de dados.
E, em terceiro lugar, o atrito de conexão.
Gerenciar a descoberta, o pareamento e a reconexão em todos os dispositivos é complexo e pode facilmente prejudicar a experiência do usuário.
Para acabar com essas dores de cabeça, criamos APIs específicas em três áreas principais para simplificar os recursos multidispositivo.
Vou começar com os apps complementares, depois o Nataraj vai falar sobre os apps de VoIP e, por fim, o Yacine vai falar sobre o compartilhamento P2P.
Vamos analisar os apps complementares primeiro.
Um app complementar é um aplicativo que você instala no smartphone para que um dispositivo complementar, como um smartwatch ou um sensor de glicose, funcione bem com ele.
Nesta seção, vamos focar nos dispositivos conectados por Bluetooth.
Normalmente, há quatro categorias de dispositivos complementares.
Primeiro, dispositivos médicos, como sensores de glicose e monitores de sinais vitais.
E wearables, como smartwatches e pulseiras fitness.
Fones de ouvido, com Pareamento Rápido e controles inteligentes.
E kits para carros, para itens como chave digital do carro e chamadas viva-voz.
Cada categoria tem as próprias jornadas do usuário, mas todas dependem de uma conexão confiável entre o dispositivo e o smartphone.
Para dispositivos médicos, essa conexão pode até ser vital.
Vamos analisar quatro necessidades comuns que identificamos nessas categorias.
Primeiro, o gerenciamento de dispositivos.
O app precisa encontrar o dispositivo certo entre todos os que estão por perto, como o relógio aqui, não o fone de ouvido ou o alto-falante, e iniciar o fluxo de pareamento para se conectar.
Em segundo lugar, a verificação de presença.
O app precisa saber quando o dispositivo está por perto e quando não está.
E, quando o dispositivo aparece, o app precisa de uma maneira de ser ativado para que possa fazer o trabalho dele, mesmo que o usuário não o tenha aberto.
Terceiro, manutenção da conexão.
Depois de conectados, os apps querem que a conexão seja confiável e persistente.
Se a conexão cair, por exemplo, se o usuário sair do alcance e voltar, o app precisará se reconectar automaticamente, mesmo que não esteja em execução no momento.
E, em quarto lugar, a sincronização e transferência de dados.
É aí que está o valor real.
Sincronizar leituras de sensores, enviar atualizações de firmware ou enviar configurações para o dispositivo.
Para dispositivos médicos, isso pode ser fundamental.
Uma leitura perdida pode significar um alerta perdido.
Todos eles precisam que o processo do app esteja ativo e conectado no momento certo.
Como os apps atendem a todas essas necessidades?
Tradicionalmente, os apps dependiam muito do serviço em primeiro plano.
Veja uma implementação típica.
Se você já criou um app complementar, isso pode parecer familiar.
Você declara as permissões de busca por Bluetooth e conexão, as permissões de serviço em primeiro plano e o tipo de serviço em primeiro plano do dispositivo conectado. Depois, você declara a permissão de notificações, porque um serviço em primeiro plano precisa mostrar uma notificação persistente.
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />Em seguida, execute um serviço fixo para manter o processo do app ativo.
class BleForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
…
return START_STICKY
}
}Isso funciona, mas consome bateria e memória porque o serviço é executado o tempo todo, mesmo quando o dispositivo não está por perto.
Por isso, criamos o Gerenciador de dispositivos complementar, ou CDM, na sigla em inglês.
Primeiro, ela pode ativar o app com base na presença do dispositivo e, enquanto o dispositivo estiver dentro do alcance, o sistema vai manter o processo do app ativo.
Não é necessário um serviço em primeiro plano.
Em segundo lugar, o CDM é compatível com perfis de dispositivo.
Com um perfil de dispositivo, o usuário concede todas as permissões necessárias para o app com um único toque, em vez de uma série de caixas de diálogo de permissão.
Isso melhora significativamente a experiência do usuário e a taxa de conversão.
O perfil de relógio está disponível desde o Android 12, e o de óculos, desde o Android 14.
No Android 17, adicionamos dois novos perfis específicos para dispositivos médicos e trackers de condicionamento físico.
Também há alguns perfis privilegiados apenas para apps do sistema.
Se houver uma necessidade mais ampla do ecossistema, podemos considerar a abertura para mais desenvolvedores.
É muito fácil usar o CDM.
Vamos usar um dispositivo médico como exemplo.
Primeiro, declare as permissões necessárias: uma para o dispositivo médico, uma para execução em segundo plano e outra para observar a presença do dispositivo.
<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_MEDICAL" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />Ao contrário das permissões que acabamos de ver com a implementação do serviço em primeiro plano, todas as permissões listadas aqui são normais, ou seja, serão concedidas na instalação, sem caixa de diálogo.
A única coisa que o usuário precisa aprovar é a associação.
Vamos ver como criar essa associação.
Aqui, criamos uma solicitação de associação com um filtro de dispositivo para que apenas o dispositivo certo apareça.
val paringRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.setSingleDevice(true)
.setDeviceProfile(AssociationRequest.DEVICE_PROFILE_MEDICAL)
.build()
companionDeviceManager.associate(paringRequest, ContextCompat.getMainExecutor(this),
object : CompanionDeviceManager.Callback() {
override fun onAssociationPending(intentSender: IntentSender) {
intentSenderLauncher.launch(IntentSenderRequest.Builder(intentSender).build())
}
override fun onAssociationCreated(associationInfo: AssociationInfo) { … }
override fun onFailure(error: CharSequence?) { … }
}
)setSingleDevice como verdadeiro, porque queremos apenas uma correspondência, e defina o perfil como médico.
Em seguida, chame associate.
O CDM processa a verificação.
Em onAssociationPending, vamos receber um IntentSender.
Inicie-o, e o sistema vai mostrar uma caixa de diálogo de associação.
Com um perfil médico, esse toque não apenas aprova a associação, mas também concede todas as permissões necessárias, até mesmo ignorando o modo Não Perturbe, o que é essencial para alertas críticos.
Depois que o usuário tocar em "Permitir", vamos receber em onAssociationCreated com AssociationInfo, que inclui um ID de associação.
Em seguida, podemos observar a presença do dispositivo chamando startObservingDevicePresence com esse ID de associação.
Também é necessário implementar um serviço que estenda CompanionDeviceService.
class BleCompanionDeviceService : CompanionDeviceService() {
override fun onDevicePresenceEvent(event: DevicePresenceEvent) {
when (event.event) {
DevicePresenceEvent.EVENT_BLE_APPEARED -> onDeviceAppeared(event.associationId)
DevicePresenceEvent.EVENT_BLE_DISAPPEARED -> onDeviceDisappeared(event.associationId)
}
}
private fun onDeviceAppeared(associationId: Int) { … }
private fun onDeviceDisappeared(associationId: Int) { … }
}Quando o dispositivo entra no alcance, o sistema se vincula ao serviço, mesmo que o app não esteja em execução.
O sistema vai chamar onDevicePresenceEvent com EVENT_BLE_APPEARED.
Depois, você pode conectar e sincronizar.
Quando o dispositivo sair, você vai receber EVENT_BLE_DISAPPEARED.
Em seguida, faça a limpeza.
Não há serviço sempre ativo nem notificações apenas para manter o app ativo.
Agora, precisamos admitir que o CDM tem algumas limitações que os desenvolvedores precisam considerar.
Primeiro, as versões do SO.
A maioria dos recursos do CDM foi lançada no Android 12 e 13.
Mas algumas melhorias legais chegaram no Android 16 e os novos perfis no Android 17.
Portanto, verifique a versão do SO durante a execução e mantenha um substituto para versões mais antigas.
Dito isso, o SDK de destino aumenta a cada ano, e o SDK mínimo tende a acompanhar.
Para lidar com a pressão de memória que os apps estão enfrentando agora e vão enfrentar no futuro, recomendamos que eles migrem do serviço em primeiro plano para o CDM o mais rápido possível.
Se você tiver um caso de uso que não seja compatível com o CDM, entre em contato.
Em segundo lugar, hoje o CDM é compatível com a verificação em segundo plano, mas não com o anúncio em segundo plano.
Raramente ouvimos falar de uma necessidade como essa.
No entanto, se o app precisar que o smartphone funcione como um periférico e anuncie continuamente em segundo plano, gostaríamos de saber mais sobre isso.
Terceiro, o CDM usa o modo de verificação de baixa energia.
Ele troca um pouco da capacidade de resposta em tempo real por bateria e memória.
Portanto, não espere ativações instantâneas.
Por fim, não há visualização de payload.
Não é possível se registrar para uma característica ou dados de anúncio específicos.
Mas, quando o dispositivo estiver dentro do alcance, o app será ativado.
Assim, ele pode se conectar e verificar o payload conforme necessário.
Isso é tudo sobre apps complementares e CDM.
Vou passar a palavra para Nataraj, que vai falar sobre apps de VoIP.
O CompanionDeviceManager é a ferramenta certa se o app tiver um periférico dedicado e se comunicar continuamente por ele.
Olá!
Sou Nataraj K R.
Agora, vou falar sobre o próximo cenário multidispositivo, que é a chamada em tempo real.
Hoje em dia, a forma como as pessoas lidam com as ligações está mudando.
Mais de 40% das chamadas estão acontecendo em redes VoIP.
E o Android tem um papel importante nisso.
Na verdade, 71% dessas ligações acontecem em dispositivos Android, o que é quase todas as chamadas.
Não são apenas as ligações.
O ecossistema de dispositivos que as pessoas têm também está crescendo muito.
Há mais de 250 milhões de carros com o Android Auto nas ruas, e também estamos enviando mais de um bilhão de dispositivos Bluetooth para os usuários.
Fones de ouvido, relógios Wear OS, carros com Android Auto e alto-falantes Bluetooth são todos os dispositivos que os usuários têm ao redor do smartphone, e eles querem que as chamadas sejam transferidas entre todos esses dispositivos sem problemas.
Por exemplo, o usuário recebe uma ligação e olha para o relógio para saber quem está ligando.
Ela atenderia a ligação no relógio.
Eles ativam ou desativam o som conforme necessário, colocam os fones de ouvido e continuam a chamada.
Se quiserem sair, eles entram no carro e esperam que a chamada mude de dispositivo para o veículo sem interrupções.
Isso se tornou uma rotina normal para o usuário, e não é uma experiência premium.
Para o usuário, esse é um requisito básico, e cabe a nós, desenvolvedores Android, oferecer essa experiência integrada nos nossos apps de VoIP.
Mas se você tentar criar tudo isso do zero, vai acabar com três grandes problemas.
Primeiro, a fragmentação com soquetes Bluetooth.
Como o sistema não sabe que seu app está processando uma chamada, você precisaria criar soquetes Bluetooth e sessões de mídia personalizados para que a chamada seja projetada no relógio ou no Android Auto.
O segundo problema é o foco de áudio.
Como você precisa lidar com APIs AudioManager e Bluetooth de baixo nível, ocorrem vazamentos de áudio, e isso não é uma prioridade para o sistema, certo?
Seu app está em uma chamada VoIP. Quando uma chamada de operadora é recebida, ela interrompe a chamada VoIP, e o modem assume o áudio, deixando o app sem som.
Pior ainda, quando a chamada celular terminar, o foco de áudio não será devolvido ao app.
E o terceiro problema é a sincronização.
O usuário tocava em "Desativar som" no relógio, mas a interface do smartphone ainda mostrava a chamada com o som ativado.
Isso cria uma grande confusão.
O usuário desconectava a chamada na coluna de direção do carro, mas o smartphone ainda mostrava uma notificação de chamada em andamento.
É uma grande frustração para o usuário quando isso acontece.
Para resolver todos esses problemas, criamos o Jetpack Telecom.
Portanto, você registra sua chamada uma vez com o Jetpack Telecom usando o CallsManager, e o Jetpack Telecom atua como um coordenador central de chamadas do sistema.
Ele é responsável por projetar sua chamada no relógio, no Android Auto e em outras plataformas remotas que o usuário tenha.
Ele também cuida do gerenciamento da seleção de áudio.
E o mais importante: tudo isso funciona perfeitamente, até mesmo no Android 8.
Projetar a chamada na tela do relógio ou do carro é só metade do processo, porque o usuário quer controlar o estado da chamada nesses dispositivos.
Eles querem ativar ou desativar o som da chamada no relógio, conforme necessário.
Eles querem desligar a chamada usando o volante do carro ou tocando duas vezes nos fones de ouvido.
Assim, a Jetpack Telecom detecta esses eventos de hardware e os apresenta como callbacks em um só lugar, que é CallControlScope.
Basta ouvir esses callbacks, processar o mecanismo de chamada e atualizar o estado da chamada de acordo.
E o Jetpack Telecom assume a responsabilidade de sincronizar esse estado de chamada entre os dispositivos.
Por isso, a biblioteca oferece benefícios em duas categorias.
Primeiro, obviamente, ele resolve todos os problemas que mencionamos.
Assim, em vez de lidar com APIs AudioManager de baixo nível, a biblioteca oferece endpoints de chamada declarativos, gerencia o foco de áudio e sincroniza os dispositivos.
Ele é responsável por transferir a chamada entre os dispositivos.
E também cuida de quando uma chamada de celular chega.
Mas o outro benefício que vem com a biblioteca é que, como ela é criada com base no Kotlin, sempre que você adiciona uma chamada com o Jetpack Telecom, ela abre um escopo de corrotina do Kotlin.
Assim, ele aproveita as corrotinas do Kotlin, que permanecem ativas exatamente até o fim da chamada.
Portanto, qualquer job executado em relação à chamada é limpo automaticamente assim que ela é desconectada.
Assim, você não terá jobs invasores, e o ciclo de vida da chamada também será gerenciado pela biblioteca.
Agora, você deve estar pensando que essa biblioteca é incrível e quer implementá-la, certo?
É muito simples.
A etapa 1 é, obviamente, declarar a dependência.
Depois, declare a permissão necessária, que é MANAGE_OWN_CALLS, e instancie um objeto CallsManager.
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />Depois de instanciar o objeto CallsManager, registre o app com os recursos que você quer oferecer, como videochamadas ou streaming de chamadas.
// Initialize CallsManager & register supported capabilities once
val callsManager = CallsManager(context)
callsManager.registerAppWithTelecom(
capabilities = CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING or
CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
)Depois, a etapa 2.
Agora que você registrou seu app no Jetpack Telecom, é hora de processar as chamadas recebidas e ligações efetuadas.
Para isso, você precisa criar um objeto CallAttributesCompat, que contém os metadados da chamada, como o displayName do autor ou destinatário da chamada, o endereço, que é o endpoint do autor da chamada, a direção, especificando se a chamada é recebida ou efetuada para que a Jetpack Telecom possa apresentar a respectiva interface nessas superfícies remotas, e, em seguida, callType, se é uma chamada de áudio ou videochamada para que você receba os recursos necessários.
val attributes = CallAttributesCompat(
displayName = "Kukk Nat",
address = Uri.parse("tel:+16505550199"),
direction = CallAttributesCompat.DIRECTION_INCOMING,
callType = CallAttributesCompat.CALL_TYPE_AUDIO_CALL
)
callsManager.addCall(
attributes, onAnswer, onDisconnect, onSetActive, onSetInactive
) {
// Coroutine CallControlScope active for the duration of the call
}Quando o objeto de metadados estiver pronto, use o método callsManager.addCall para adicioná-lo.
Isso abre um escopo de corrotina de controle de chamadas.
Depois de adicionar a chamada, vamos para a etapa 3.
A etapa 3 é detectar as ações que o usuário vai realizar nessas plataformas remotas.
Como você tem CallControlScope, basta coletar os fluxos do Kotlin.
callsManager.addCall(attributes, onAnswer, onDisconnect, onSetActive, onSetInactive) {
// 1. React to hardware mute toggles from Watch, Car, or Headset
launch {
isMuted.collect { muted -> audioEngine.setMicMuted(muted) }
}
// 2. Observe available endpoints & request declarative switches
launch {
currentCallEndpoint.collect { endpoint -> updateUi(endpoint) }
}
requestEndpointChange(preferredBluetoothEndpoint)
}Colete isMuted para reagir sempre que o usuário desativar o som da chamada nessas plataformas remotas e, assim, atualizar o estado da chamada.
Colete currentCallEndpoint para saber em qual endpoint a chamada está acontecendo e atualizar a IU de acordo.
Você pode mostrar esse dispositivo específico na interface do smartphone.
E sempre que o usuário quiser mudar de um dispositivo para outro, basta chamar o método requestEndpointChange com o endpoint preferido que o usuário quiser.
É simples assim.
Com essas três etapas, seu app está pronto para usar a Jetpack Telecom integrada.
Mas não é só isso.
Com o Jetpack Telecom 1.1.0, estamos trazendo muito mais recursos, como históricos de ligações unificados, o que significa que todas as suas chamadas VoIP agora vão aparecer de forma nativa no discador do sistema.
Assim, a vantagem é que os usuários poderão instanciar uma chamada de retorno com um toque diretamente do discador do sistema para o app.
Assim, não é preciso procurar o app específico entre todos os outros.
Eles podem simplesmente retornar a ligação usando o discador do sistema que já estão acostumados a usar.
Mas, para alguns, mas se você, mas também estamos fornecendo controles granulares.
Se você não quiser que uma chamada específica seja registrada no sistema, basta definir a flag isLogExcluded como verdadeira no objeto CallAttributesCompat.
O objeto de metadados que mencionei anteriormente, basta definir como verdadeiro, e sua chamada não será registrada no sistema nem vai aparecer no discador do sistema.
O segundo recurso incrível que vai surgir com a versão 1.1.0 é o fracionamento de rede 5G.
Nas redes de operadoras compatíveis, o que acontece é que as redes oferecem fatias de rede premium. Isso significa que, para um caso de uso específico, a operadora oferece uma fatia de rede com baixa latência e sem outras interrupções, para que você tenha uma melhor qualidade e experiência.
Portanto, nas redes de operadoras compatíveis, você terá esse recurso pronto para uso, sem precisar de nenhum esforço extra, nem mesmo uma única linha de código.
Depois de integrar a biblioteca, suas chamadas vão aproveitar essas fatias de rede, e você terá uma latência menor e uma performance melhor.
Mas, se por algum motivo você não quiser esse recurso, use a flag CAPABILITY_OPT_OUT_OF_PREMIUM_NETWORK para desativá-lo.
O Jetpack Telecom já foi implementado pelos principais apps de comunicação globais, como Google Meet, WhatsApp, KakaoTalk e Zoho Arattai.
Agora é com você. Agora é com você integrar essa biblioteca ao seu app e oferecer uma experiência melhor e mais integrada para o usuário.
Agora que já falamos sobre as chamadas em tempo real, vou passar a palavra para meu colega Yacine, que vai falar sobre o recurso de tocar para compartilhar.
Nossa equipe agradece.
Pessoal, sei que vocês estão um pouco cansados, mas não se preocupem, esta é a última seção.
Vou falar sobre o compartilhamento de arquivos, mas há outros casos de uso para o compartilhamento peer-to-peer.
No ecossistema de dispositivos móveis, mais de 14 bilhões de fotos e vídeos são compartilhados em redes sociais e apps de mensagens todos os dias. É uma quantidade enorme.
E todos esses arquivos são compartilhados na maioria das vezes pela nuvem. Você tira a foto, faz o upload dela pelo seu serviço favorito e a pessoa precisa fazer o download da mesma forma do outro lado.
E o mesmo acontece quando você tenta convidar alguém ou um amigo para uma rede social, para colaborar em uma playlist ou documento. Geralmente, você precisa digitar o nome de usuário ou o e-mail, o que leva um tempo no teclado.
Você pode cometer um erro, e isso não é uma experiência muito boa, principalmente quando vocês estão literalmente sentados um ao lado do outro. É tipo, ah, tenho que enviar pela nuvem, tenho que esperar.
Não é muito bom.
Hoje em dia, todos nós temos smartphones, dispositivos, tablets e dobráveis sofisticados, e tiramos fotos incríveis com eles.
Mas, quando você compartilha por algumas dessas redes sociais ou apps de mensagens, por problemas de desempenho e armazenamento, elas são compactadas. No final, na sua tela incrível, elas acabam ficando pixeladas ou sem a melhor qualidade.
Essa não é a melhor experiência que você gostaria de ter no seu dispositivo premium.
Em segundo lugar, os arquivos agora são bem grandes.
Uma única foto pode ter facilmente 10 MB, e um vídeo de 5 minutos pode ter cerca de 500 MB. Por isso, o upload e o download de tudo isso levam bastante tempo.
Por fim, do outro lado, você precisa esperar essa latência.
Às vezes, você pode estar em um ônibus lotado e, enquanto ele se move, a latência não é das melhores, a cobertura de rede é ruim e a transferência de arquivos é interrompida. Às vezes, devido a essa latência muito longa, pode ocorrer um tempo limite e a transferência de arquivos é cancelada, o que é muito frustrante para o usuário.
Então, em vez de depender da nuvem para compartilhar todos esses arquivos e ter que esperar por tudo isso, por que não usar uma rede local, como Bluetooth ou Wi-Fi?
Mas se você tentar fazer isso por conta própria, obviamente, terá que lidar com o gerenciador de Bluetooth, o gerenciador de Wi-Fi, ter que escrever seu próprio protocolo, ou seja, nem todo mundo quer perder tempo fazendo isso.
Mas, na verdade, já existe algo no Android.
Ele já está disponível e talvez você já tenha ouvido falar dele: o Quick Share.
O Quick Share no Android permite que você tenha essa abstração já implementada no sistema e possa compartilhar arquivos com outro dispositivo sem precisar gerenciar tudo isso.
Ele já está disponível na sua página de compartilhamento e alterna entre os protocolos para escolher o melhor para seu dispositivo. Ele está disponível no Marshmallow e versões mais recentes, o que, hoje em dia, é um público enorme, certo?
Por fim, essa interface e experiência do usuário que você vê ao lado realmente ajudam seus usuários.
Eles já sabem o que é a interface, não precisam aprender a usá-la, já a conhecem.
Mas sabemos que os desenvolvedores também querem ter controle sobre a experiência.
Você conhece seu app e seus usuários melhor do que nós, provavelmente.
Então, por que não ter o Quick Share diretamente no seu app?
Então, conversamos com desenvolvedores e vimos que o Quick Share já é bastante usado.
Ele está disponível no Android, em Chromebooks, recentemente no Google Livros e também no Windows, ou seja, em mais de 3 bilhões de dispositivos. É enorme.
E também trouxemos com o Pixel 11 a capacidade de tocar para compartilhar.
Assim, em vez de escolher o dispositivo com que você quer compartilhar, basta tocar e, com o NFC, fazemos essa detecção do dispositivo diretamente para você.
Você pode selecionar uma foto ou um arquivo, tocar em dois dispositivos e compartilhar diretamente com um toque.
E, novamente, conhecemos desenvolvedores que querem ter controle sobre a experiência.
Sabemos que essa é a melhor maneira de os usuários usarem um recurso específico, neste caso, o compartilhamento de arquivos.
Então, por que não incorporar o Quick Share ao seu app?
Por isso, no ano que vem, vamos disponibilizar o SDK do Quick Share diretamente para você.
Você poderá usá-lo diretamente no seu app, e ele já vem com segurança integrada, como criptografia de ponta a ponta entre os dois dispositivos.
Como as transferências acontecem entre essas redes locais, você terá a velocidade máxima, obviamente no Wi-Fi da sua casa, mas também entre vocês dois, basta criar um ponto de acesso automaticamente, você não precisa lidar com isso.
E para dispositivos compatíveis com o Wi-Fi Direct, ele já está incluído.
E os arquivos que você tirou com sua câmera incrível vão manter a melhor resolução e os detalhes, para que você possa vê-los bem na tela grande.
E, como ele depende de redes locais, você pode usá-lo no metrô, mesmo na parte mais profunda, e ainda terá rede para compartilhar com seu amigo.
No avião, você vai estar a não sei quantos mil metros de altura, no céu, e ainda vai poder compartilhar esses arquivos.
Por fim, para você, como desenvolvedor, é uma enorme economia de custos em termos de dados, porque, primeiro, seus usuários não precisam usar o plano de dados, e isso é muito importante quando eles estão viajando como turistas.
Além disso, você não precisa manter isso nos seus próprios servidores.
No ano passado, mencionamos aos desenvolvedores e usuários que, com o Quick Share, eles poderão compartilhar arquivos com dispositivos iPhone pelo AirDrop.
Com o Quick Share, estamos trazendo o mesmo para os desenvolvedores.
Vamos ter um SDK do Quick Share para iOS para que os desenvolvedores que têm apps iOS possam compartilhar arquivos entre as duas plataformas, independente do dispositivo.
Tudo isso é uma apresentação sofisticada. Agora, vamos dar uma olhada rápida no código.
Tentamos simplificar ao máximo.
Você inicia um QuickShareClient, anexa os arquivos que quer compartilhar e, ao iniciar a sessão, aparece essa janela modal que você está vendo ao lado, a página inferior em que os usuários podem ver os arquivos que estão sendo enviados como prévias. Se você considerar o conteúdo sensível, também poderá ativar essa opção na tela.
// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val attachment = FileAttachment(
fileName = "report.pdf",
size = 1_048_576L,
uri = fileUri,
mimeType = "application/pdf",
// Sensitive content will be masked in preview
isSensitiveContent = false,
)
val content = ShareableContent.withFiles(listOf(attachment))
val request = SharingRequest.withContent(content)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)Quando você quiser receber um arquivo, novamente, no seu app, é a mesma coisa. Claro, nesse caso, você não está adicionando um anexo e, assim que a sessão é iniciada, não precisa lidar com nada disso.
// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val request = SharingRequest.withoutContent()
val session = quickShareClient.startSharingSession(request, lifecycleOwner)
// Open the Quick Share overlay in receive mode
session.launchWindow(activity, contentSelectionIntent = null)Até agora, você viu literalmente apenas 20 linhas de código em ambos os lados.
Sabemos que, com o Quick Share e o aproxime para compartilhar, a capacidade de compartilhar arquivos apenas tocando nos dispositivos também possibilita um novo caso de uso.
Seu app pode estar fechado no outro dispositivo e, ao tocar, assim que o arquivo for transferido, ele será aberto automaticamente e você vai receber o URI para fazer algum processamento no dispositivo, por exemplo, ou mostrar algo ao usuário diretamente no app.
// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val urlAttachment = TextAttachment(
text = groupAddUrl,
type = TextAttachment.Type.URL,
)
val content = ShareableContent.withText(urlAttachment)
val appLink = AppLink("https://myapp.com/group/invite")
val request = SharingRequest.withContent(content, appLink)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)Portanto, não é necessário abrir o app no dispositivo receptor.
Com o aproxime para compartilhar, ele será aberto automaticamente.
E, claro, se você quiser que sua IU reaja a essa transferência de arquivos, basta usar um fluxo do Kotlin para ter todos os eventos necessários para processar isso.
// Final API may undergo further updates
scope.launch {
for (event in session.sharingEvents) {
when (event) {
is SharingEvent.SendComplete ->
println("Send finished")
is SharingEvent.ReceiveComplete ->
println("Received, files=${event.content?.files?.size}")
else -> println("other event")
}
}
}Já vimos muita coisa hoje, então vou encerrar.
E não se preocupe, você terá alguns minutos para fazer uma pausa.
O CompanionDeviceManager é a melhor API para lidar com qualquer interação Bluetooth, inclusive as contínuas, como Hanrui apresentou anteriormente.
Se o app tiver chamadas de voz sobre IP, não precisa ser o caso de uso principal. Por exemplo, você pode ter clientes de suporte como parte do app em que o usuário pode ligar para o suporte.
Nesse caso, a melhor maneira de lidar com chamadas de voz sobre IP no Android é com o Jetpack Telecom.
Por fim, para compartilhar arquivos entre dispositivos ou abrir apps, como mencionei, o Quick Share é a solução.
Então, agradeço a todos e, antes de irem, só um aviso rápido: vamos estar lá fora depois para responder às perguntas de vocês.
Temos horários de atendimento para tirar dúvidas sobre todos os casos de uso que apresentamos.
E não se esqueça de compartilhar o que aprendeu na sua palestra ou em outras sessões, obviamente, nas redes sociais.
Temos o #AndroidDevSummit.
É muito importante para nós saber que temos engajamento, que as pessoas estão realmente gostando do evento.
Isso nos ajuda a saber que temos uma comunidade que queremos ver de novo.
E pegue seu pingente social no balcão de informações para adicionar ao chaveiro do bot do Android.
Fiz certo?
Nossa equipe agradece.
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />class BleForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
…
return START_STICKY
}
}<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_MEDICAL" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />val paringRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.setSingleDevice(true)
.setDeviceProfile(AssociationRequest.DEVICE_PROFILE_MEDICAL)
.build()
companionDeviceManager.associate(paringRequest, ContextCompat.getMainExecutor(this),
object : CompanionDeviceManager.Callback() {
override fun onAssociationPending(intentSender: IntentSender) {
intentSenderLauncher.launch(IntentSenderRequest.Builder(intentSender).build())
}
override fun onAssociationCreated(associationInfo: AssociationInfo) { … }
override fun onFailure(error: CharSequence?) { … }
}
)class BleCompanionDeviceService : CompanionDeviceService() {
override fun onDevicePresenceEvent(event: DevicePresenceEvent) {
when (event.event) {
DevicePresenceEvent.EVENT_BLE_APPEARED -> onDeviceAppeared(event.associationId)
DevicePresenceEvent.EVENT_BLE_DISAPPEARED -> onDeviceDisappeared(event.associationId)
}
}
private fun onDeviceAppeared(associationId: Int) { … }
private fun onDeviceDisappeared(associationId: Int) { … }
}<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />// Initialize CallsManager & register supported capabilities once
val callsManager = CallsManager(context)
callsManager.registerAppWithTelecom(
capabilities = CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING or
CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
)val attributes = CallAttributesCompat(
displayName = "Kukk Nat",
address = Uri.parse("tel:+16505550199"),
direction = CallAttributesCompat.DIRECTION_INCOMING,
callType = CallAttributesCompat.CALL_TYPE_AUDIO_CALL
)
callsManager.addCall(
attributes, onAnswer, onDisconnect, onSetActive, onSetInactive
) {
// Coroutine CallControlScope active for the duration of the call
}callsManager.addCall(attributes, onAnswer, onDisconnect, onSetActive, onSetInactive) {
// 1. React to hardware mute toggles from Watch, Car, or Headset
launch {
isMuted.collect { muted -> audioEngine.setMicMuted(muted) }
}
// 2. Observe available endpoints & request declarative switches
launch {
currentCallEndpoint.collect { endpoint -> updateUi(endpoint) }
}
requestEndpointChange(preferredBluetoothEndpoint)
}// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val attachment = FileAttachment(
fileName = "report.pdf",
size = 1_048_576L,
uri = fileUri,
mimeType = "application/pdf",
// Sensitive content will be masked in preview
isSensitiveContent = false,
)
val content = ShareableContent.withFiles(listOf(attachment))
val request = SharingRequest.withContent(content)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val request = SharingRequest.withoutContent()
val session = quickShareClient.startSharingSession(request, lifecycleOwner)
// Open the Quick Share overlay in receive mode
session.launchWindow(activity, contentSelectionIntent = null)// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val urlAttachment = TextAttachment(
text = groupAddUrl,
type = TextAttachment.Type.URL,
)
val content = ShareableContent.withText(urlAttachment)
val appLink = AppLink("https://myapp.com/group/invite")
val request = SharingRequest.withContent(content, appLink)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)// Final API may undergo further updates
scope.launch {
for (event in session.sharingEvents) {
when (event) {
is SharingEvent.SendComplete ->
println("Send finished")
is SharingEvent.ReceiveComplete ->
println("Received, files=${event.content?.files?.size}")
else -> println("other event")
}
}
}