1. Antes de comenzar
¿No sería fantástico si pudieras usar tu dispositivo móvil para colaborar en proyectos grupales o compartir videos, transmitir contenido o disfrutar de un juego multijugador, incluso sin acceso a Internet? En realidad, sí puedes. Y en este codelab aprenderás a hacerlo.
Para evitar complicaciones, vamos a compilar un juego multijugador de piedra, papel o tijera, que funciona sin Internet. En este codelab, aprenderás a usar la API de Conexiones de Nearby, que forma parte de los Servicios de Google Play, a fin de permitir que los usuarios se comuniquen entre sí según su proximidad física. Estos se deben encontrar a aproximadamente 100 metros de distancia. No hay límite para el tipo o la cantidad de datos que pueden compartir los usuarios, incluso sin conexión a Internet. Pueden transmitir videos en streaming, enviar y recibir mensajes de voz, enviar mensajes de texto y mucho más.
Requisitos previos
- Conocimientos básicos sobre desarrollo en Kotlin y para Android
- Cómo crear y ejecutar apps en Android Studio
- Dos o más dispositivos Android para ejecutar y probar el código
- en Android con nivel de API 16 o posterior
- con los Servicios de Google Play instalados
- La versión más reciente de Android Studio
Qué aprenderás
- Cómo agregar la biblioteca de Conexiones de Nearby de los Servicios de Google Play a tu app
- Cómo anunciar tu interés por comunicarte con dispositivos cercanos
- Cómo descubrir dispositivos cercanos que te interesen
- Cómo comunicarte con dispositivos conectados
- Prácticas recomendadas de privacidad y protección de datos
Qué compilarás
En este codelab, te enseñaremos a compilar una sola app de actividad que permita a un usuario encontrar rivales y jugar a piedra, papel o tijera. La IU de la app tiene los siguientes elementos:
- Un botón para encontrar rivales
- Un control de juego con tres botones que permiten a los usuarios elegir PIEDRA, PAPEL o TIJERA para jugar.
- Objetos
TextView
para mostrar puntuaciones - Un objeto
TextView
para mostrar estados
Figura 1
2. Cómo crear un proyecto de Android Studio
- Inicia un proyecto nuevo de Android Studio.
- Selecciona Empty Activity.
- Asígnale al proyecto el nombre Piedra, Papel o Tijera y establece Kotlin como lenguaje.
3. Código de configuración
- Agrega la versión más reciente de la dependencia de Nearby al archivo
build.gradle
del nivel de la app. Esto permite que tu app use la API de Conexiones de Nearby para anunciar interés en conectarse, descubrir dispositivos cercanos y comunicarse.
implementation 'com.google.android.gms:play-services-nearby:LATEST_VERSION'
- Configura la opción de compilación viewBinding como
true
en el bloqueandroid
para habilitar la vinculación de vista. De este modo, no tienes que usarfindViewById
para interactuar con vistas.
android {
...
buildFeatures {
viewBinding true
}
}
- Haz clic en Sync Now o en el botón de martillo verde para que Android Studio tenga en cuenta estos cambios de Gradle.
- Usaremos elementos de diseño vectoriales para nuestras imágenes de piedra, papel o tijera. Agrega los siguientes tres archivos XML a tu directorio
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>
- Agrega los objetos
TextView
de control del juego (en otras palabras, botones de juego), la puntuación y los estados para la pantalla del juego. En el archivoactivity_main.xml
, reemplaza el código por lo siguiente:
<?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>
Tu diseño ahora debería verse como en la figura 1 (más arriba).
Nota: En tu propio proyecto, cambia los valores como 16dp
a recursos como @dimen/activity_vertical_margin
.
4. Agrega conexiones de Nearby a tu app
Prepara tu archivo manifest.xml
Agrega los siguientes permisos al archivo de manifiesto. Debido a que ACCESS_FINE_LOCATION
es un permiso riesgoso, tu app incluirá código que activará el sistema para solicitar a los usuarios, en nombre de tu app, que otorguen o denieguen el acceso. Se aplican los permisos de Wi-Fi a las conexiones entre pares, no a las 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"/>
Cómo elegir una estrategia
La API de Conexiones de Nearby exige que elijas una Strategy
que determine cómo se conecta tu app con otros dispositivos cercanos. Elige P2P_CLUSTER
, P2P_STAR
o P2P_POINT_TO_POINT
.
Para nuestros fines, elegiremos P2P_STAR
porque nos conviene ver muchas solicitudes entrantes de jugadores que quieren desafiarnos, pero solo jugar contra una persona por vez.
La Strategy
que elijas debe usarse para las funciones de anuncio y descubrimiento en tu app. En la siguiente figura, se muestra cómo funciona cada Strategy
.
Los dispositivos pueden solicitar N conexiones salientes | Los dispositivos pueden recibir M conexiones entrantes | |||
| N = muchas | M = muchas | Se generan conexiones con un ancho de banda más bajo | |
| N = 1 | M = muchas | Se generan conexiones con un mayor ancho de banda | |
| N = 1 | M = 1 | Mayor capacidad de procesamiento posible |
Define variables en MainActivity
- Dentro de la actividad principal (
MainActivity.kt
), sobre la funciónonCreate()
, pega el siguiente fragmento de código para definir las siguientes variables. Estas variables definen la lógica específica del juego y los permisos de tiempo de ejecución.
/**
* 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
- Cambia la función
onCreate()
para pasar el objetoViewBinding
asetContentView()
. De esta manera, se muestra el contenido del archivo de diseñoactivity_main.xml
. Además, inicializa el objetoconnectionsClient
para que tu app pueda comunicarse con la API.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
connectionsClient = Nearby.getConnectionsClient(this)
}
Verifica los permisos necesarios
Como regla general, se declaran los permisos riesgosos en el archivo AndroidManifest.xml
, pero se deben solicitar durante el tiempo de ejecución. De igual manera, debes verificar los demás permisos necesarios durante el tiempo de ejecución a fin de garantizar que el resultado sea el esperado. Si el usuario deniega alguno de los permisos, muestra un aviso para informarle que no puede continuar sin otorgar esos permisos, ya que la app de ejemplo no se puede usar sin ellos.
- Debajo de la función
onCreate()
, agrega el siguiente fragmento de código para verificar si contamos con los permisos:
@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()
}
}
En este punto, escribimos código para realizar las siguientes tareas:
- Creamos nuestro archivo de diseño
- Declaramos los permisos necesarios en nuestro manifiesto
- Verificamos los permisos riesgosos necesarios durante el tiempo de ejecución
Explicación invertida
Ahora que ya nos ocupamos de los preliminares, estamos listos para empezar a escribir el código de Conexiones de Nearby para conectarnos y comunicarnos con los usuarios cercanos. Por lo general, antes de que puedas comunicarte con los dispositivos cercanos, tu app debe permitir que otros dispositivos la encuentren y buscar otros.
En otras palabras, en el contexto de nuestro juego de piedra, papel o tijera, debes conectarte con tus rivales para comenzar a jugar.
Puedes hacer que tu dispositivo sea detectable mediante un proceso llamado anuncio. De manera similar, puedes descubrir oponentes cercanos gracias a un proceso llamado descubrimiento.
A fin de comprender el proceso, es mejor abordar el código en orden inverso. Por eso, procederemos de la siguiente manera:
- Supongamos que ya estamos conectados y escribimos el código para enviar y recibir mensajes. En relación con nuestro objetivo actual, esto significa escribir el código para jugar el juego de piedra, papel o tijera.
- Escribiremos el código para anunciar que nos interesa conectarnos con dispositivos cercanos.
- Escribiremos el código para detectar dispositivos cercanos.
Cómo enviar y recibir datos
Usa el método connectionsClient.sendPayload()
para enviar datos como Payload
y el objeto PayloadCallback
para recibir las cargas útiles. Una Payload
puede constar de diferentes contenidos: video, fotos, transmisiones o cualquier otro tipo de datos. Y no hay límite de datos.
- En nuestro juego, la carga útil es una elección de piedra, papel o tijera. Cuando un usuario hace clic en uno de los botones del control, la app envía su elección a la app del rival en forma de carga útil. Para registrar el movimiento del usuario, agrega el siguiente fragmento de código debajo de la función
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
}
}
- Un dispositivo recibe cargas útiles a través del objeto
PayloadCallback
, que tiene dos métodos. El métodoonPayloadReceived()
le indica a la app cuándo recibe un mensaje y el métodoonPayloadTransferUpdate()
realiza un seguimiento del estado de los mensajes entrantes y salientes.
En nuestro caso, leeremos el mensaje entrante de onPayloadReceived()
como el movimiento de nuestro rival y usaremos el método onPayloadTransferUpdate()
para realizar un seguimiento y confirmar cuando ambos jugadores hayan hecho sus movimientos. Agrega este fragmento de código por encima del 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)
}
}
}
Anuncio
Anuncias tu presencia o interés con la intención de que una persona cercana te detecte y solicite conectarse contigo. Como tal, el método startAdvertising()
de la API de Conexiones de Nearby requiere un objeto de devolución de llamada. Esa devolución de llamada, ConnectionLifecycleCallback
, te informa cuando una persona que vio tu anuncio quiere conectarse. El objeto de devolución de llamada tiene tres métodos:
- El método
onConnectionInitiated()
indica que una persona vio tu anuncio y quiere conectarse. En consecuencia, puedes aceptar la conexión conconnectionsClient.acceptConnection()
. - Cuando una persona detecta tu anuncio, te envía una solicitud de conexión. Tanto tú como la otra persona deben aceptar la solicitud de conexión para conectarse. El método
onConnectionResult()
te permite saber si se estableció la conexión. - La función
onDisconnected()
te indica que la conexión ya no está activa. Esto puede ocurrir, por ejemplo, si tú o el rival deciden terminar la conexión.
Para anunciar tu interés, haz lo siguiente:
- En nuestra app, aceptaremos la conexión cuando recibamos la devolución de llamada de
onConnectionInitiated()
. Luego, dentro del métodoonConnectionResult()
, si se estableció la conexión, dejaremos de usar las funciones de anuncio y descubrimiento, ya que solo necesitamos conectarnos con un rival para jugar. Por último, enonConnectionResult()
, restableceremos el juego.
Pega el siguiente fragmento de código antes del 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()
}
}
- Debido a que el método
resetGame()
es sumamente práctico para hacer llamadas en diferentes momentos, lo usaremos en su propia subrutina. Agrega el código en la parte inferior de la claseMainActivity
.
/** 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 = ":"
}
- El siguiente fragmento de código es la llamada de anuncio real, en la que se le indica a la API de Conexiones de Nearby que quieres ingresar al modo de anuncio. Agrégalo debajo del 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
)
}
Descubrimiento
El complemento de la función de anuncio es la de descubrimiento. Las dos llamadas son muy similares, excepto que usan devoluciones de llamada diferentes. La devolución de llamada de la llamada startDiscovery()
es un objeto EndpointDiscoveryCallback
. Este tiene dos métodos de devolución de llamada: se llama a onEndpointFound()
cada vez que se detecta un anuncio; se llama a onEndpointLost()
cada vez que un anuncio ya no está disponible.
- Nuestra app se conectará con el primer anunciante que detectemos. Esto significa que haremos una solicitud de conexión dentro del método
onEndpointFound()
y no haremos nada con respecto al métodoonEndpointLost()
. Agrega la devolución de llamada antes del 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) {
}
}
- Además, debes agregar el fragmento para que se le indique a la API de Conexiones de Nearby que quieres ingresar al modo de descubrimiento. Agrégalo en la parte inferior de la clase
MainActivity
.
private fun startDiscovery(){
val options = DiscoveryOptions.Builder().setStrategy(STRATEGY).build()
connectionsClient.startDiscovery(packageName,endpointDiscoveryCallback,options)
}
- En este punto, la parte de nuestro trabajo con Conexiones de Nearby está lista. Podemos anunciarnos, descubrir dispositivos cercanos y comunicarnos con ellos. Pero aún no pudimos jugar. Debemos terminar de ajustar las vistas de la IU:
- Cuando el usuario hace clic en el botón FIND OPPONENT, la app debe llamar a los métodos
startAdvertising()
ystartDiscovery()
. De esta manera, descubres y te descubren. - Cuando el usuario hace clic en uno de los botones del control, ROCK, PAPER, o SCISSORS, la app debe llamar al método
sendGameChoice()
para transmitir datos al rival. - Cuando cualquiera de los usuarios haga clic en el botón DISCONNECT, la app debería restablecer el juego.
Actualiza el método onCreate()
para reflejar estas interacciones.
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
}
Limpieza
Deberías dejar de usar la API de Nearby cuando ya no sea necesario. En el caso de nuestro juego de muestra, liberamos todos los recursos dentro de la función del ciclo de vida de la actividad de onStop()
.
@CallSuper
override fun onStop(){
connectionsClient.apply {
stopAdvertising()
stopDiscovery()
stopAllEndpoints()
}
resetGame()
super.onStop()
}
5. Ejecuta la app
Ejecuta la app en dos dispositivos y disfruta del juego.
6. Prácticas recomendadas de privacidad
Nuestro juego de piedra, papel o tijera no comparte datos sensibles. Incluso los nombres internos se generan de forma aleatoria. Por eso, aceptamos automáticamente la conexión dentro del objeto onConnectionInitiated(String, ConnectionInfo)
.
El objeto ConnectionInfo
contiene un token único por conexión, al que tu app puede acceder a través del método getAuthenticationDigits()
. Puedes mostrar los tokens a ambos usuarios para realizar una verificación visual. Como alternativa, puedes encriptar el token sin procesar en un dispositivo y enviarlo como una carga útil para desencriptarlo en el otro antes de comenzar a compartir datos sensibles. Si quieres obtener más información sobre la encriptación en Android, consulta esta entrada de blog llamada "Mejora la criptografía de tu app, desde la autenticación de mensajes hasta la presencia de los usuarios" (disponible en inglés).
7. Felicitaciones
¡Felicitaciones! Ahora sabes cómo conectar a tus usuarios sin conexión a Internet a través de la API de Conexiones de Nearby.
En resumen, para usar la API de Conexiones de Nearby, debes agregar una dependencia para play-services-nearby
. También debes solicitar permisos en el archivo AndroidManifest.xml
y verificarlos durante el tiempo de ejecución. También aprendiste a hacer lo siguiente:
- Anunciar tu interés en conectarte con usuarios cercanos
- Descubrir usuarios cercanos que desean conectarse
- Aceptar conexiones
- Enviar mensajes
- Recibir mensajes
- Proteger la privacidad del usuario
¿Qué sigue?
Consulta la serie de blogs y ejemplos