Comunicação bidirecional sem Internet

1. Antes de começar

Não seria ótimo poder usar seu dispositivo móvel para colaborar em projetos em grupo ou compartilhar vídeos, fazer streaming de conteúdo e jogar com outras pessoas, mesmo sem acesso à Internet? Você pode! Neste codelab, você aprenderá a fazer exatamente isso.

Para simplificar, criaremos um jogo multiplayer de "Pedra, papel e tesoura" que funciona sem acesso à Internet. Este codelab ensina a usar a API Nearby Connections, parte do Google Play Services, para permitir que os usuários se comuniquem entre si com base na proximidade física. A distância entre eles precisa ser menor que 100 metros. Não há limite para o tipo ou a quantidade de dados que eles podem compartilhar, mesmo sem uma conexão de Internet. Os usuários podem fazer streaming de vídeos, enviar e receber mensagens de voz, enviar mensagens de texto e muito mais.

Pré-requisitos

  • Ter conhecimento básico de desenvolvimento Android e Kotlin.
  • Saber como criar e executar apps no Android Studio.
  • Ter dois ou mais dispositivos Android para executar e testar o código.
  • Usar a API do Android de nível 16 ou mais recente.
  • Ter o Google Play Services instalado.
  • Ter a versão mais recente do Android Studio.

O que você aprenderá

  • Como adicionar a biblioteca Nearby Connections do Google Play Services ao app.
  • Como divulgar seu interesse de se comunicar com dispositivos por perto.
  • Como descobrir dispositivos por perto que sejam do seu interesse.
  • Como se comunicar com dispositivos conectados.
  • Práticas recomendadas de privacidade e proteção de dados.

O que você criará

Este codelab mostra como criar um único app de atividade para que o usuário possa encontrar adversários e jogar "Pedra, papel e tesoura". O app tem os seguintes elementos de IU:

  1. Um botão para encontrar adversários
  2. Um controle de jogo com três botões, que permitem que os usuários escolham ROCK (pedra), PAPER (papel) ou SCISSORS (tesoura)
  3. TextViews para exibir pontuações
  4. Uma TextView para mostrar o status

625eeebfad3b195a.png

Figura 1

2. Criar um projeto do Android Studio

  1. Inicie um novo projeto no Android Studio.
  2. Escolha Empty Activity.

f2936f15aa940a21.png

  1. Nomeie o projeto como Pedra, papel e tesoura e defina a linguagem como "Kotlin".

1ea410364fbdfc31.png

3. Configurar o código

  1. Adicione a versão mais recente da dependência Nearby ao arquivo build.gradle do seu app. Isso permite que o app use a API Nearby Connections para divulgar o interesse em se conectar, descobrir dispositivos por perto e se comunicar.
implementation 'com.google.android.gms:play-services-nearby:LATEST_VERSION'
  1. Defina a opção de build viewBinding como true no bloco android para ativar a vinculação de visualizações. Assim, você não precisa usar findViewById para interagir com visualizações.
android {
   ...
   buildFeatures {
       viewBinding true
   }
}
  1. Clique em Sync Now ou no botão de martelo verde para que o Android Studio considere essas mudanças no Gradle.

57995716c771d511.png

  1. Estamos usando drawables vetoriais para nossas imagens de pedra, papel e tesoura. Adicione os três arquivos XML a seguir ao diretório res/drawable.

res/drawables/rock.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:height="24dp"
   android:tintMode="multiply"
   android:viewportHeight="48.0"
   android:viewportWidth="48.0"
   android:width="24dp">
 <path
     android:fillColor="#ffffff"
     android:pathData="M28,12l-7.5,10 5.7,7.6L23,32c-3.38,-4.5 -9,-12 -9,-12L2,36h44L28,12z"/>
</vector>

res/drawables/paper.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:height="24dp"
   android:tintMode="multiply"
   android:viewportHeight="48.0"
   android:viewportWidth="48.0"
   android:width="24dp">
 <path
     android:fillColor="#ffffff"
     android:pathData="M28,4L12,4C9.79,4 8.02,5.79 8.02,8L8,40c0,2.21 1.77,4 3.98,4L36,44c2.21,0 4,-1.79 4,-4L40,16L28,4zM26,18L26,7l11,11L26,18z"/>
</vector>

res/drawables/scissors.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:width="24dp"
   android:height="24dp"
   android:tintMode="multiply"
   android:viewportWidth="48.0"
   android:viewportHeight="48.0">
   <path
       android:fillColor="#ffffff"
       android:pathData="M19.28,15.28c0.45,-1 0.72,-2.11 0.72,-3.28 0,-4.42 -3.58,-8 -8,-8s-8,3.58 -8,8 3.58,8 8,8c1.17,0 2.28,-0.27 3.28,-0.72L20,24l-4.72,4.72c-1,-0.45 -2.11,-0.72 -3.28,-0.72 -4.42,0 -8,3.58 -8,8s3.58,8 8,8 8,-3.58 8,-8c0,-1.17 -0.27,-2.28 -0.72,-3.28L24,28l14,14h6v-2L19.28,15.28zM12,16c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4zM12,40c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4zM24,25c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM38,6L26,18l4,4L44,8L44,6z" />
</vector>
  1. Adicione o controle de jogo (ou seja, os botões), a pontuação e as TextViews de status para a tela do jogo. No arquivo activity_main.xml, substitua o código pelo seguinte:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <TextView
       android:id="@+id/status"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:padding="16dp"
       android:text="searching..."
       app:layout_constraintBottom_toTopOf="@+id/myName"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       />

   <TextView
       android:id="@+id/myName"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_gravity="center"
       android:text="You (codeName)"
       android:textAlignment="center"
       android:textAppearance="?android:textAppearanceMedium"
       app:layout_constraintEnd_toStartOf="@+id/opponentName"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/status"
       />

   <TextView
       android:id="@+id/opponentName"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_gravity="center"
       android:text="Opponent (codeName)"
       android:textAlignment="center"
       android:textAppearance="?android:textAppearanceMedium"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/myName"
       app:layout_constraintTop_toBottomOf="@+id/status"
       />

   <TextView
       android:id="@+id/score"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_gravity="center"
       android:layout_margin="16dp"
       android:text=":"
       android:textAlignment="center"
       android:textSize="80sp"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/myName"
       />

   <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/rock"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:drawableTop="@drawable/rock"
       android:text="Rock"
       app:layout_constraintEnd_toStartOf="@+id/paper"
       app:layout_constraintHorizontal_chainStyle="spread"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/score"
       />

   <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/paper"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:drawableTop="@drawable/paper"
       android:text="Paper"
       app:layout_constraintEnd_toStartOf="@+id/scissors"
       app:layout_constraintStart_toEndOf="@+id/rock"
       app:layout_constraintTop_toBottomOf="@+id/score"
       />

   <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/scissors"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:drawableTop="@drawable/scissors"
       android:text="Scissors"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/paper"
       app:layout_constraintTop_toBottomOf="@+id/score"
       />

   <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/disconnect"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_margin="32dp"
       android:text="disconnect"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/paper"
       />

   <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/findOpponent"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_margin="32dp"
       android:text="find opponent"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/paper"
       />

</androidx.constraintlayout.widget.ConstraintLayout>

Seu layout ficará parecido com a Figura 1 mostrada acima.

Observação: no seu projeto, mude valores como 16dp para recursos como @dimen/activity_vertical_margin.

4. Adicionar a Nearby Connections ao app

Preparar o arquivo manifest.xml

Adicione estas permissões ao arquivo de manifesto. Como a ACCESS_FINE_LOCATION é uma permissão perigosa, seu app vai incluir um código que acionará o sistema para solicitar aos usuários o acesso em nome do app. As permissões de Wi-Fi se aplicam a conexões ponto a ponto, não de Internet.

<!-- Required for Nearby Connections →

<!--    Because ACCESS_FINE_LOCATION is a dangerous permission, the app will have to-->
<!--    request it at runtime, and the user will be prompted to grant or deny access.-->

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

Como escolher uma estratégia

A API Nearby Connections exige que você escolha uma Strategy que determine como seu app se conecta a outros dispositivos por perto. Escolha P2P_CLUSTER, P2P_STAR ou P2P_POINT_TO_POINT.

Para nossos objetivos, vamos escolher P2P_STAR, porque queremos poder ver várias solicitações de jogadores que querem nos desafiar, mas jogar contra uma pessoa de cada vez.

A Strategy escolhida precisa ser usada para divulgação e descobertas no app. A figura abaixo mostra como cada Strategy funciona.

Os dispositivos podem solicitar N conexões de saída

Os dispositivos podem receber M conexões de entrada

P2P_CLUSTER

N = várias

M = várias

resulta em conexões com largura de banda menor

P2P_STAR

N = 1

M = várias

resulta em conexões com largura de banda maior

P2P_POINT_TO_POINT

N = 1

M = 1

capacidade de processamento máxima

Definir variáveis na MainActivity

  1. Dentro da atividade principal (MainActivity.kt), acima da função onCreate(), defina as seguintes variáveis colando este snippet de código. Essas variáveis definem a lógica específica do jogo e as permissões de execução.
/**
* Enum class for defining the winning rules for Rock-Paper-Scissors. Each player will make a
* choice, then the beats function in this class will be used to determine whom to reward the
* point to.
*/
private enum class GameChoice {
   ROCK, PAPER, SCISSORS;

   fun beats(other: GameChoice): Boolean =
       (this == ROCK && other == SCISSORS)
               || (this == SCISSORS && other == PAPER)
               || (this == PAPER && other == ROCK)
}

/**
* Instead of having each player enter a name, in this sample we will conveniently generate
* random human readable names for players.
*/
internal object CodenameGenerator {
   private val COLORS = arrayOf(
       "Red", "Orange", "Yellow", "Green", "Blue", "Indigo", "Violet", "Purple", "Lavender"
   )
   private val TREATS = arrayOf(
       "Cupcake", "Donut", "Eclair", "Froyo", "Gingerbread", "Honeycomb",
       "Ice Cream Sandwich", "Jellybean", "Kit Kat", "Lollipop", "Marshmallow", "Nougat",
       "Oreo", "Pie"
   )
   private val generator = Random()

   /** Generate a random Android agent codename  */
   fun generate(): String {
       val color = COLORS[generator.nextInt(COLORS.size)]
       val treat = TREATS[generator.nextInt(TREATS.size)]
       return "$color $treat"
   }
}

/**
* Strategy for telling the Nearby Connections API how we want to discover and connect to
* other nearby devices. A star shaped strategy means we want to discover multiple devices but
* only connect to and communicate with one at a time.
*/
private val STRATEGY = Strategy.P2P_STAR

/**
* Our handle to the [Nearby Connections API][ConnectionsClient].
*/
private lateinit var connectionsClient: ConnectionsClient

/**
* The request code for verifying our call to [requestPermissions]. Recall that calling
* [requestPermissions] leads to a callback to [onRequestPermissionsResult]
*/
private val REQUEST_CODE_REQUIRED_PERMISSIONS = 1

/*
The following variables are convenient ways of tracking the data of the opponent that we
choose to play against.
*/
private var opponentName: String? = null
private var opponentEndpointId: String? = null
private var opponentScore = 0
private var opponentChoice: GameChoice? = null

/*
The following variables are for tracking our own data
*/
private var myCodeName: String = CodenameGenerator.generate()
private var myScore = 0
private var myChoice: GameChoice? = null

/**
* This is for wiring and interacting with the UI views.
*/
private lateinit var binding: ActivityMainBinding
  1. Mude a função onCreate() para transmitir o objeto ViewBinding para setContentView(). Isso exibirá o conteúdo do arquivo de layout activity_main.xml. Além disso, inicialize a connectionsClient para que seu app possa se comunicar com a API.
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
   connectionsClient = Nearby.getConnectionsClient(this)
}

Verificar as permissões necessárias

Como regra geral, as permissões perigosas são declaradas no arquivo AndroidManifest.xml, mas precisam ser solicitadas no momento da execução. Para outras permissões necessárias, ainda é preciso verificá-las no momento da execução para garantir que o resultado seja o esperado. Se o usuário negar qualquer uma delas, mostre um aviso informando que não é possível continuar sem conceder essas permissões, já que nosso app de exemplo não pode ser usado sem elas.

  • Abaixo da função onCreate(), adicione o seguinte snippet de código para verificar se temos as permissões:
@CallSuper
override fun onStart() {
   super.onStart()
   if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
       requestPermissions(
           arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
           REQUEST_CODE_REQUIRED_PERMISSIONS
       )
   }
}

@CallSuper
override fun onRequestPermissionsResult(
   requestCode: Int,
   permissions: Array<out String>,
   grantResults: IntArray
) {
   super.onRequestPermissionsResult(requestCode, permissions, grantResults)
   val errMsg = "Cannot start without required permissions"
   if (requestCode == REQUEST_CODE_REQUIRED_PERMISSIONS) {
       grantResults.forEach {
           if (it == PackageManager.PERMISSION_DENIED) {
               Toast.makeText(this, errMsg, Toast.LENGTH_LONG).show()
               finish()
               return
           }
       }
       recreate()
   }
}

Agora, já programamos o código para realizar estas tarefas:

  • Criar nosso arquivo de layout
  • Declarar as permissões necessárias no manifesto
  • Verificar as permissões perigosas necessárias no momento da execução

Tutorial em ordem reversa

Agora que já cuidamos das preliminares, está tudo pronto para começar a programar o código da Nearby Connections para nos conectarmos e nos comunicarmos com usuários próximos. Normalmente, antes de você começar a se comunicar com dispositivos por perto, o app precisa permitir que os dispositivos o encontrem e buscar dispositivos.

Em outras palavras, você e seus adversários precisam encontrar uns aos outros antes de começar a jogar nosso Pedra, papel e tesoura.

Você pode tornar seu dispositivo detectável usando um processo chamado divulgação. Da mesma forma, você pode descobrir adversários próximos usando um processo conhecido como descoberta.

Para entender o processo, é recomendável lidar com o código na ordem reversa. Para isso, prosseguiremos da seguinte maneira:

  1. Vamos fingir que já temos uma conexão e programaremos o código para enviar e receber mensagens. Para nossa finalidade atual, isso significa programar o código para jogar Pedra, papel e tesoura.
  2. Programaremos o código para divulgar nosso interesse em nos conectarmos a dispositivos por perto.
  3. Vamos programar o código para descobrir dispositivos por perto.

Como enviar e receber dados

Use o método connectionsClient.sendPayload() para enviar dados como Payload, e o objeto PayloadCallback para receber os payloads. Um Payload pode ser qualquer coisa: vídeos, fotos, streams ou qualquer outro tipo de dado. E não há limite de dados.

  1. No nosso jogo, o payload é uma escolha de pedra, papel ou tesoura. Quando um usuário clica em um dos botões do controle, o app envia a escolha dele ao app do adversário como um payload. Para registrar o movimento do usuário, adicione o seguinte snippet de código abaixo da função onRequestPermissionsResult().
/** Sends the user's selection of rock, paper, or scissors to the opponent. */
private fun sendGameChoice(choice: GameChoice) {
   myChoice = choice
   connectionsClient.sendPayload(
       opponentEndpointId!!,
       Payload.fromBytes(choice.name.toByteArray(UTF_8))
   )
   binding.status.text = "You chose ${choice.name}"
   // For fair play, we will disable the game controller so that users don't change their
   // choice in the middle of a game.
   setGameControllerEnabled(false)
}

/**
* Enables/Disables the rock, paper and scissors buttons. Disabling the game controller
* prevents users from changing their minds after making a choice.
*/
private fun setGameControllerEnabled(state: Boolean) {
   binding.apply {
       rock.isEnabled = state
       paper.isEnabled = state
       scissors.isEnabled = state
   }
}
  1. Um dispositivo recebe payloads pelo objeto PayloadCallback, que tem dois métodos. onPayloadReceived() informa ao app quando ele está recebendo uma mensagem, e onPayloadTransferUpdate() monitora o status das mensagens recebidas e enviadas.

Para nossos objetivos, vamos ler a mensagem recebida do onPayloadReceived() como a jogada do adversário e usar o método onPayloadTransferUpdate() para monitorar e confirmar quando os dois jogadores tiverem feito um movimento. Adicione esse snippet de código acima do método onCreate().

/** callback for receiving payloads */
private val payloadCallback: PayloadCallback = object : PayloadCallback() {
   override fun onPayloadReceived(endpointId: String, payload: Payload) {
       payload.asBytes()?.let {
           opponentChoice = GameChoice.valueOf(String(it, UTF_8))
       }
   }

   override fun onPayloadTransferUpdate(endpointId: String, update: PayloadTransferUpdate) {
       // Determines the winner and updates game state/UI after both players have chosen.
       // Feel free to refactor and extract this code into a different method
       if (update.status == PayloadTransferUpdate.Status.SUCCESS
           && myChoice != null && opponentChoice != null) {
           val mc = myChoice!!
           val oc = opponentChoice!!
           when {
               mc.beats(oc) -> { // Win!
                   binding.status.text = "${mc.name} beats ${oc.name}"
                   myScore++
               }
               mc == oc -> { // Tie
                   binding.status.text = "You both chose ${mc.name}"
               }
               else -> { // Loss
                   binding.status.text = "${mc.name} loses to ${oc.name}"
                   opponentScore++
               }
           }
           binding.score.text = "$myScore : $opponentScore"
           myChoice = null
           opponentChoice = null
           setGameControllerEnabled(true)
       }
   }
}

Você divulga sua presença ou seu interesse na esperança de que alguém por perto encontre seu dispositivo e peça para se conectar com você. Por isso, o método startAdvertising() da API Nearby Connections exige um objeto de callback. Esse callback, ConnectionLifecycleCallback, informa quando alguém que viu sua divulgação quer se conectar. O objeto de callback tem três métodos:

  • O método onConnectionInitiated() informa que alguém viu sua divulgação e quer se conectar. Você poderá aceitar a conexão com connectionsClient.acceptConnection().
  • Quando alguém vê sua divulgação, essa pessoa envia a você uma solicitação de conexão. Para se conectar, você e o remetente precisam aceitar a solicitação. O método onConnectionResult() informa se a conexão foi estabelecida.
  • A função onDisconnected() informa que a conexão não está mais ativa. Isso pode acontecer, por exemplo, se você ou o adversário decidir encerrar a conexão.

Para fazer a divulgação:

  1. Para nosso app, aceitaremos a conexão quando recebermos o callback onConnectionInitiated(). Como só precisávamos nos conectar a um adversário, interromperemos a divulgação e a descoberta dentro do onConnectionResult() se a conexão tiver sido estabelecida. Por fim, redefiniremos o jogo no método onConnectionResult().

Cole o snippet de código a seguir antes do método onCreate().

// Callbacks for connections to other devices
private val connectionLifecycleCallback = object : ConnectionLifecycleCallback() {
   override fun onConnectionInitiated(endpointId: String, info: ConnectionInfo) {
       // Accepting a connection means you want to receive messages. Hence, the API expects
       // that you attach a PayloadCall to the acceptance
       connectionsClient.acceptConnection(endpointId, payloadCallback)
       opponentName = "Opponent\n(${info.endpointName})"
   }

   override fun onConnectionResult(endpointId: String, result: ConnectionResolution) {
       if (result.status.isSuccess) {
           connectionsClient.stopAdvertising()
           connectionsClient.stopDiscovery()
           opponentEndpointId = endpointId
           binding.opponentName.text = opponentName
           binding.status.text = "Connected"
           setGameControllerEnabled(true) // we can start playing
       }
   }

   override fun onDisconnected(endpointId: String) {
       resetGame()
   }
}
  1. Como o resetGame() é tão conveniente para chamar em momentos diversos, nós o colocaremos na própria sub-rotina. Adicione o código na parte de baixo da classe MainActivity.
/** Wipes all game state and updates the UI accordingly. */
private fun resetGame() {
   // reset data
   opponentEndpointId = null
   opponentName = null
   opponentChoice = null
   opponentScore = 0
   myChoice = null
   myScore = 0
   // reset state of views
   binding.disconnect.visibility = View.GONE
   binding.findOpponent.visibility = View.VISIBLE
   setGameControllerEnabled(false)
   binding.opponentName.text="opponent\n(none yet)"
   binding.status.text ="..."
   binding.score.text = ":"
}
  1. O snippet a seguir é a chamada de divulgação real, em que você informa à API Nearby Connections que quer entrar no modo de divulgação. Adicione-o abaixo do método onCreate().
private fun startAdvertising() {
   val options = AdvertisingOptions.Builder().setStrategy(STRATEGY).build()
   // Note: Advertising may fail. To keep this demo simple, we don't handle failures.
   connectionsClient.startAdvertising(
       myCodeName,
       packageName,
       connectionLifecycleCallback,
       options
   )
}

Descoberta

O complemento da divulgação é a descoberta. As duas chamadas são muito parecidas, mas usam callbacks diferentes. O callback para a chamada startDiscovery() é um objeto EndpointDiscoveryCallback. Esse objeto tem dois métodos de callback: o onEndpointFound() é chamado sempre que uma divulgação é detectada, e o onEndpointLost() é chamado sempre que uma divulgação não está mais disponível.

  1. Nosso app se conectará com o primeiro divulgador que detectarmos. Isso significa que faremos uma solicitação de conexão dentro do método onEndpointFound() e não faremos nada com o método onEndpointLost(). Adicione o callback antes do método onCreate().
// Callbacks for finding other devices
private val endpointDiscoveryCallback = object : EndpointDiscoveryCallback() {
   override fun onEndpointFound(endpointId: String, info: DiscoveredEndpointInfo) {
       connectionsClient.requestConnection(myCodeName, endpointId, connectionLifecycleCallback)
   }

   override fun onEndpointLost(endpointId: String) {
   }
}
  1. Adicione também o snippet para informar à API Nearby Connections que você quer entrar no modo de descoberta. Adicione-o à parte inferior da classe MainActivity.
private fun startDiscovery(){
   val options = DiscoveryOptions.Builder().setStrategy(STRATEGY).build()
   connectionsClient.startDiscovery(packageName,endpointDiscoveryCallback,options)
}
  1. Terminamos a parte da Nearby Connections do nosso trabalho. Já podemos divulgar, descobrir e nos comunicarmos com dispositivos por perto. No entanto, ainda não podemos jogar. Precisamos concluir a configuração das visualizações da IU:
  • Quando o usuário clicar no botão FIND OPPONENT, o app precisará chamar startAdvertising() e startDiscovery(). Assim, seu dispositivo poderá descobrir e ser descoberto.
  • Quando o usuário clicar em um dos botões de ROCK (pedra), PAPER (papel) ou SCISSORS (tesouras), o app precisará chamar o método sendGameChoice() para transmitir esses dados ao adversário.
  • Quando o usuário clicar no botão DISCONNECT, o app precisará redefinir o jogo.

Atualize seu método onCreate() para refletir essas interações.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
   connectionsClient = Nearby.getConnectionsClient(this)

   binding.myName.text = "You\n($myCodeName)"
   binding.findOpponent.setOnClickListener {
       startAdvertising()
       startDiscovery()
       binding.status.text = "Searching for opponents..."
       // "find opponents" is the opposite of "disconnect" so they don't both need to be
       // visible at the same time
       binding.findOpponent.visibility = View.GONE
       binding.disconnect.visibility = View.VISIBLE
   }
   // wire the controller buttons
   binding.apply {
       rock.setOnClickListener { sendGameChoice(GameChoice.ROCK) }
       paper.setOnClickListener { sendGameChoice(GameChoice.PAPER) }
       scissors.setOnClickListener { sendGameChoice(GameChoice.SCISSORS) }
   }
   binding.disconnect.setOnClickListener {
       opponentEndpointId?.let { connectionsClient.disconnectFromEndpoint(it) }
       resetGame()
   }

   resetGame() // we are about to start a new game
}

Limpeza

Pare de usar a API Nearby quando ela não for mais necessária. Para nosso exemplo, liberamos todos os recursos dentro da função onStop() de ciclo de vida da atividade.

@CallSuper
override fun onStop(){
   connectionsClient.apply {
       stopAdvertising()
       stopDiscovery()
       stopAllEndpoints()
   }
   resetGame()
   super.onStop()
}

5. Executar o app

Execute o app em dois dispositivos e aproveite o jogo.

e545703b29e0158a.gif

6. Práticas recomendadas de privacidade

Nosso jogo Pedra, papel e tesoura não compartilha dados sensíveis. Até os codinomes são gerados aleatoriamente. É por isso que aceitamos automaticamente a conexão dentro de onConnectionInitiated(String, ConnectionInfo).

O objeto ConnectionInfo contém um token exclusivo por conexão, que seu app pode acessar pelo getAuthenticationDigits(). Você pode exibir os tokens para os dois usuários para verificação visual. Como alternativa, você pode criptografar o token bruto em um dispositivo e enviá-lo como um payload a ser descriptografado no outro dispositivo antes de começar a compartilhar dados sensíveis. Para saber mais sobre criptografia no Android, confira a postagem do blog Melhorar a criptografia do seu app, da autenticação de mensagens à presença do usuário (em inglês).

7. Parabéns

Parabéns! Agora você sabe conectar seus usuários sem Internet usando a API Nearby Connections.

Em resumo, para usar a API Nearby Connections, você precisa adicionar uma dependência de play-services-nearby. Também é necessário pedir permissões no arquivo AndroidManifest.xml e verificá-las no momento da execução. Você também aprendeu a fazer o seguinte:

  • Divulgar seu interesse em se conectar com usuários próximos
  • Encontrar usuários próximos que queiram se conectar
  • Aceitar conexões
  • Enviar mensagens
  • Receber mensagens
  • Proteger a privacidade do usuário

Qual é a próxima etapa?

Confira nossa série de postagens do blog e um exemplo.