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:
- Um botão para encontrar adversários
- Um controle de jogo com três botões, que permitem que os usuários escolham ROCK (pedra), PAPER (papel) ou SCISSORS (tesoura)
TextView
s para exibir pontuações- Uma
TextView
para mostrar o status
Figura 1
2. Criar um projeto do Android Studio
- Inicie um novo projeto no Android Studio.
- Escolha Empty Activity.
- Nomeie o projeto como Pedra, papel e tesoura e defina a linguagem como "Kotlin".
3. Configurar o código
- 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'
- Defina a opção de build viewBinding como
true
no blocoandroid
para ativar a vinculação de visualizações. Assim, você não precisa usarfindViewById
para interagir com visualizações.
android {
...
buildFeatures {
viewBinding true
}
}
- Clique em Sync Now ou no botão de martelo verde para que o Android Studio considere essas mudanças no Gradle.
- 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>
- Adicione o controle de jogo (ou seja, os botões), a pontuação e as
TextView
s de status para a tela do jogo. No arquivoactivity_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 | |||
| N = várias | M = várias | resulta em conexões com largura de banda menor | |
| N = 1 | M = várias | resulta em conexões com largura de banda maior | |
| N = 1 | M = 1 | capacidade de processamento máxima |
Definir variáveis na MainActivity
- Dentro da atividade principal (
MainActivity.kt
), acima da funçãoonCreate()
, 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
- Mude a função
onCreate()
para transmitir o objetoViewBinding
parasetContentView()
. Isso exibirá o conteúdo do arquivo de layoutactivity_main.xml
. Além disso, inicialize aconnectionsClient
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:
- 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.
- Programaremos o código para divulgar nosso interesse em nos conectarmos a dispositivos por perto.
- 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.
- 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
}
}
- Um dispositivo recebe payloads pelo objeto
PayloadCallback
, que tem dois métodos.onPayloadReceived()
informa ao app quando ele está recebendo uma mensagem, eonPayloadTransferUpdate()
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)
}
}
}
Divulgação
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 comconnectionsClient.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:
- 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 doonConnectionResult()
se a conexão tiver sido estabelecida. Por fim, redefiniremos o jogo no métodoonConnectionResult()
.
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()
}
}
- 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 classeMainActivity
.
/** 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 = ":"
}
- 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.
- 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étodoonEndpointLost()
. Adicione o callback antes do métodoonCreate()
.
// 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) {
}
}
- 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)
}
- 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()
estartDiscovery()
. 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.
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.