Comunicación bidireccional sin Internet

¿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:

  1. Un botón para encontrar rivales
  2. Un control de juego con tres botones que permiten a los usuarios elegir PIEDRA, PAPEL o TIJERA para jugar.
  3. Objetos TextView para mostrar puntuaciones
  4. Un objeto TextView para mostrar estados

625eeebfad3b195a.png

Figura 1

  1. Inicia un proyecto nuevo de Android Studio.
  2. Selecciona Empty Activity.

f2936f15aa940a21.png

  1. Asígnale al proyecto el nombre Piedra, Papel o Tijera y establece Kotlin como lenguaje.

1ea410364fbdfc31.png

  1. 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'
  1. Configura la opción de compilación viewBinding como true en el bloque android para habilitar la vinculación de vista. De este modo, no tienes que usar findViewById para interactuar con vistas.
android {
   ...
   buildFeatures {
       viewBinding true
   }
}
  1. Haz clic en Sync Now o en el botón de martillo verde para que Android Studio tenga en cuenta estos cambios de Gradle.

57995716c771d511.png

  1. 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>
  1. 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 archivo activity_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.

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

P2P_CLUSTER

N = muchas

M = muchas

Se generan conexiones con un ancho de banda más bajo

P2P_STAR

N = 1

M = muchas

Se generan conexiones con un mayor ancho de banda

P2P_POINT_TO_POINT

N = 1

M = 1

Mayor capacidad de procesamiento posible

Define variables en MainActivity

  1. Dentro de la actividad principal (MainActivity.kt), sobre la función onCreate(), 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
  1. Cambia la función onCreate() para pasar el objeto ViewBinding a setContentView(). De esta manera, se muestra el contenido del archivo de diseño activity_main.xml. Además, inicializa el objeto connectionsClient 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:

  1. 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.
  2. Escribiremos el código para anunciar que nos interesa conectarnos con dispositivos cercanos.
  3. 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.

  1. 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
   }
}
  1. Un dispositivo recibe cargas útiles a través del objeto PayloadCallback, que tiene dos métodos. El método onPayloadReceived() le indica a la app cuándo recibe un mensaje y el método onPayloadTransferUpdate() 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)
       }
   }
}

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 con connectionsClient.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:

  1. En nuestra app, aceptaremos la conexión cuando recibamos la devolución de llamada de onConnectionInitiated(). Luego, dentro del método onConnectionResult(), 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, en onConnectionResult(), 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()
   }
}
  1. 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 clase 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. 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.

  1. 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étodo onEndpointLost(). Agrega la devolución de llamada antes del 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. 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)
}
  1. 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() y startDiscovery(). 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()
}

Ejecuta la app en dos dispositivos y disfruta del juego.

e545703b29e0158a.gif

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

¡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