Cómo agregar Nearby Messages a tu app

Imagina que estás en un centro comercial lleno de gente y encuentras a tu amiga Jane. Tu inclinación podría ser saludar y gritar para llamar la atención de Jane. La API de Nearby Messages de Google está aquí para que tu app se encargue de los saludos por los usuarios a fin de que los amigos puedan descubrirse con facilidad entre sí cuando estén cerca físicamente. En este codelab, aprenderás a usar la API de Nearby Messages para habilitar las interacciones de usuarios en función de la proximidad física. A fin de que el codelab sea sencillo, cada usuario publicará el modelo de compilación de su teléfono: android.os.Build.MODEL. Sin embargo, en realidad, podrías hacer que cada usuario publicara su userId, o cualquier información adecuada de tu caso de uso, para amigos cercanos. Para ofrecer esta función, la API de Nearby Messages combina la conexión a Internet, Bluetooth y otras tecnologías.

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

Qué aprenderás

  • Cómo agregar la biblioteca de Nearby a la app
  • Cómo transmitir mensajes a grupos interesados
  • Cómo detectar mensajes de lugares de interés
  • Cómo obtener una clave de API para tus mensajes
  • Prácticas recomendadas para la duración de batería

Requisitos

  • Una Cuenta de Google (es decir, una dirección de Gmail) para obtener una clave de API de Google
  • La versión más reciente de Android Studio
  • Dos dispositivos Android con los Servicios de Google Play (en otras palabras, Play Store) instalados
  • Una conexión a Internet (a diferencia de la API de Nearby Connections, que no la requiere)

Qué compilarás

Una sola app de Activity que permite al usuario publicar información del dispositivo y recibir datos sobre dispositivos cercanos. La app tiene dos interruptores que el usuario puede activar o desactivar: el primero sirve para descubrir o dejar de descubrir mensajes cercanos; el segundo sirve para publicar o dejar de publicar mensajes. Para esta app, queremos que se detengan la publicación y el descubrimiento después de 120 segundos arbitrarios. Para ello, analizaremos un poco más la API, crearemos un objeto PublishOptions y un objeto SubscribeOptions, y usamos sus devoluciones de llamada a onExpired() para desactivar los interruptores de IU de publicación y suscripción.

56bd91ffed49ec3d.png

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

f2936f15aa940a21.png

  1. Asígnale el nombre Nearby Messages Example al proyecto y elige Kotlin como el lenguaje.

3220c65e598bf6af.png

  1. Agrega la versión más reciente de la dependencia de Nearby al archivo build.gradle del nivel de la app. De esta manera, puedes usar la API de Nearby Messages para enviar y detectar mensajes de dispositivos cercanos.
implementation 'com.google.android.gms:play-services-nearby:18.0.0'
  1. Establece la opción de compilación viewBinding en true, en el bloque de Android para habilitar ViewBinding.
android {
   ...
   buildFeatures {
       viewBinding true
   }
}
  1. Haz clic en Sync Now o en el botón de martillo verde para permitir que Android Studio registre estos cambios de Gradle.

57995716c771d511.png

  1. Agrega los botones de activación "Discover nearby devices" y "Share device information" y la the RecycleView que contendrá la lista de dispositivos. En el archivo activity_main.xml, reemplaza el código por lo siguiente:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/activity_main_container"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical"
   android:padding="16dp"
   tools:context=".MainActivity">

   <androidx.appcompat.widget.SwitchCompat
       android:id="@+id/subscribe_switch"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="Discover nearby devices" />

   <androidx.appcompat.widget.SwitchCompat
       android:id="@+id/publish_switch"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="Share device information" />

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/nearby_msg_recycler_view"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:transcriptMode="alwaysScroll" />
</LinearLayout>

Nota: En tu propio proyecto, cambia los valores como 16dp a recursos como @dimen/activity_vertical_margin.

Cómo definir variables

  1. Dentro de la actividad principal (MainActivity.kt), sobre la función onCreate(), pega el siguiente fragmento de código para definir las variables a continuación.
/**
* For accessing layout variables
*/
private lateinit var binding: ActivityMainBinding

/**
* Sets the time to live in seconds for the publish or subscribe.
*/
private val TTL_IN_SECONDS = 120 // Two minutes.

/**
* Choose of strategies for publishing or subscribing for nearby messages.
*/
private val PUB_SUB_STRATEGY = Strategy.Builder().setTtlSeconds(TTL_IN_SECONDS).build()

/**
* The [Message] object used to broadcast information about the device to nearby devices.
*/
private lateinit var message: Message

/**
* A [MessageListener] for processing messages from nearby devices.
*/
private lateinit var messageListener: MessageListener

/**
* MessageAdapter is a custom class that we will define later. It's for adding
* [messages][Message] to the [RecyclerView]
*/
private lateinit var msgAdapter: MessageAdapter

Definimos un elemento Strategy porque queremos personalizar la duración de una transmisión. Para este codelab, elegimos 120 segundos. Si no especificas una estrategia, la API usa los valores predeterminados. Además, aunque en este codelab usamos la misma Strategy para la publicación y la suscripción, no es obligatorio que hagas lo mismo.

  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.
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

Botones de IU de Wire

Esta app realizará tres acciones: publicar mensajes con solo presionar un botón, descubrir mensajes con solo presionar un botón y mostrarlos en una RecyclerView.

  1. Queremos que el usuario publique mensajes y anule su publicación, y que descubra mensajes (es decir, se suscriba a ellos). Por ahora, crea métodos de stub para esto llamados publish(), unpublish(), subscribe() y unsubscribe(). Crearemos la implementación en un paso posterior.
private fun publish() {
   TODO("Not yet implemented")
}

private fun unpublish() {
   TODO("Not yet implemented")
}

private fun subscribe() {
   TODO("Not yet implemented")
}

private fun unsubscribe() {
   TODO("Not yet implemented")
}
  1. El usuario puede publicar o descubrir mensajes (es decir, suscribirse a ellos) mediante los botones que se agregaron al diseño de la actividad. Une los dos Switches para llamar a los métodos que definimos al final de la función onCreate().
   binding.subscribeSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
       if (isChecked) {
           subscribe()
       } else {
           unsubscribe()
       }
   }

   binding.publishSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
       if (isChecked) {
           publish()
       } else {
           unpublish()
       }
   }
  1. Ahora que agregaste el código de IU para publicar y descubrir mensajes, configura RecyclerView a fin de mostrar y quitar mensajes. RecyclerView mostrará los mensajes que se publican de forma activa. El suscriptor estará a la escucha de mensajes. Cuando se encuentre un mensaje, el suscriptor lo agregará a RecyclerView. Cuando se pierda un mensaje, es decir, el publicador deje de publicarlo, el suscriptor lo quitará de RecyclerView.
private fun setupMessagesDisplay() {
   msgAdapter = MessageAdapter()
   with(binding.nearbyMsgRecyclerView) {
       layoutManager = LinearLayoutManager(context)
       this.adapter = msgAdapter
   }
}

class MessageAdapter : RecyclerView.Adapter<MessageAdapter.MessageVH>() {
   private var itemsList: MutableList<String> = arrayListOf()

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageVH {
       return MessageVH(TextView(parent.context))
   }

   override fun onBindViewHolder(holder: MessageVH, position: Int) {
       holder.bind(getItem(position))
   }

   override fun getItemCount(): Int = itemsList.size

   private fun getItem(pos: Int): String? = if (itemsList.isEmpty()) null else itemsList[pos]

   fun addItem(item: String) {
       itemsList.add(item)
       notifyItemInserted(itemsList.size)
   }

   fun removeItem(item: String) {
       val pos = itemsList.indexOf(item)
       itemsList.remove(item)
       notifyItemRemoved(pos)
   }

   inner class MessageVH(private val tv: TextView) : RecyclerView.ViewHolder(tv) {
       fun bind(item: String?) {
           item?.let { tv.text = it }
       }
   }
}
  1. Al final de la función onCreate(), agrega una llamada a la función setupMessagesDisplay().
override fun onCreate(savedInstanceState: Bundle?) {
   ...
   setupMessagesDisplay()
}

Ahora que la IU está configurada, podemos comenzar a publicar mensajes para que descubran otros dispositivos cercanos. La vista en tu app debería ser la siguiente:

56bd91ffed49ec3d.png

Cómo agregar código de publicación y descubrimiento

  1. Para enviar un mensaje, primero necesitamos un objeto Message. Como se trata de una demostración, solo enviamos el modelo del dispositivo. Agrega este código a la función onCreate() para crear el mensaje que se enviará.
override fun onCreate(savedInstanceState: Bundle?) {
   ...

   // The message being published is simply the Build.MODEL of the device. But since the
   // Messages API is expecting a byte array, you must convert the data to a byte array.
   message = Message(Build.MODEL.toByteArray(Charset.forName("UTF-8")))

}
  1. Para publicar un Message que puedan descubrir otros dispositivos cercanos, solo tienes que llamar a Nearby.getMessagesClient(activity).publish(message). Sin embargo, te recomendamos ir más allá y crear tu propio objeto PublishOptions. Esto te permite especificar tu propia Strategy personalizada y aprovechar el PublishCallback, que informa cuando venció un mensaje publicado. En el siguiente código, creamos una opción que permita desactivar el botón por el usuario cuando venza el TTL publicado. Luego, pasaremos la opción cuando llamemos a publish(). Actualiza tu función publish() de la siguiente manera:
private fun publish() {
   val options = PublishOptions.Builder()
       .setStrategy(PUB_SUB_STRATEGY)
       .setCallback(object : PublishCallback() {
           override fun onExpired() {
               super.onExpired()
               // flick the switch off since the publishing has expired.
               // recall that we had set expiration time to 120 seconds
               // Use runOnUiThread to force the callback
               // to run on the UI thread
               runOnUiThread{
                   binding.publishSwitch.isChecked = false
               }
           }
       }).build()

   Nearby.getMessagesClient(this).publish(message, options)
}

Este código se ejecuta cada vez que el usuario ACTIVA el interruptor de publicación.

  1. Si bien la publicación requiere un Message, la suscripción requiere un MessageListener. Pero aquí también recomendamos que compiles un objeto SubscribeOptions, aunque no sea necesario para que funcione la API. Por ejemplo, si compilas tu propia SubscriptionOption, podrás especificar durante cuánto tiempo quieres permanecer en el modo de descubrimiento.

Agrega el siguiente código MessageListener a la función onCreate(). Cuando se detecta un mensaje, el objeto de escucha lo agrega a RecyclerView. Cuando se pierde un mensaje, el objeto de escucha lo quita de RecyclerView.

messageListener = object : MessageListener() {
   override fun onFound(message: Message) {
       // Called when a new message is found.
       val msgBody = String(message.content)
       msgAdapter.addItem(msgBody)
   }

   override fun onLost(message: Message) {
       // Called when a message is no longer detectable nearby.
       val msgBody = String(message.content)
       msgAdapter.removeItem(msgBody)
   }
}
  1. Técnicamente, un suscriptor no necesita un TTL (o puede establecer su TTL en infinity). En este codelab, queremos detener el descubrimiento después de 120 segundos. En consecuencia, compilaremos nuestro propio elemento SubscribeOptions y usaremos su devolución de llamada a onExpired() para desactivar Switch de la IU de suscripción. Actualiza tu función de suscripción con este código.
private fun subscribe() {
   val options = SubscribeOptions.Builder()
       .setStrategy(PUB_SUB_STRATEGY)
       .setCallback(object : SubscribeCallback() {
           override fun onExpired() {
               super.onExpired()
               // flick the switch off since the subscribing has expired.
               // recall that we had set expiration time to 120 seconds
               // Use runOnUiThread to force the callback
               // to run on the UI thread
               runOnUiThread {
                   binding.subscribeSwitch.isChecked = false
               }
           }
       }).build()

   Nearby.getMessagesClient(this).subscribe(messageListener, options)
}
  1. Es importante que los usuarios puedan desactivar el uso compartido de información. Esto significa permitir que los publicadores dejen de publicar y que los suscriptores dejen de suscribirse. Para dejar de publicar, un publicador debe especificar el mensaje que desea dejar de publicar. Por lo tanto, si se están transmitiendo diez mensajes, puedes dejar de publicar uno, de modo que queden nueve.
private fun unpublish() {
   Nearby.getMessagesClient(this).unpublish(message)
}
  1. Si bien una app puede publicar varios mensajes al mismo tiempo, solo puede tener un MessageListener a la vez. Por lo tanto, es más general anular la suscripción. Para detener la suscripción, un suscriptor debe especificar el objeto de escucha.
private fun unsubscribe() {
   Nearby.getMessagesClient(this).unsubscribe(messageListener)
}
  1. Como nota al margen, aunque la API debe detener sus procesos cuando se cierra el proceso del cliente, es posible que quieras dejar de suscribirte (y publicar si es conveniente) en el método del ciclo de vida de onDestroy().
override fun onDestroy() {
   super.onDestroy()
   // although the API should shutdown its processes when the client process dies,
   // you may want to stop subscribing (and publishing if convenient)
   Nearby.getMessagesClient(this).unpublish(message)
   Nearby.getMessagesClient(this).unsubscribe(messageListener)
}

En este punto, nuestro código debería compilarse. Sin embargo, la app no funcionará como se espera debido a que falta la clave de API. Tu MainActivity debe tener la siguiente apariencia:

package com.example.nearbymessagesexample

import android.os.Build
import android.os.Bundle
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.nearbymessagesexample.databinding.ActivityMainBinding
import com.google.android.gms.nearby.Nearby
import com.google.android.gms.nearby.messages.Message
import com.google.android.gms.nearby.messages.MessageListener
import com.google.android.gms.nearby.messages.PublishCallback
import com.google.android.gms.nearby.messages.PublishOptions
import com.google.android.gms.nearby.messages.Strategy
import com.google.android.gms.nearby.messages.SubscribeCallback
import com.google.android.gms.nearby.messages.SubscribeOptions
import java.nio.charset.Charset

class MainActivity : AppCompatActivity() {

   /**
    * For accessing layout variables
    */
   private lateinit var binding: ActivityMainBinding

   /**
    * Sets the time to live in seconds for the publish or subscribe.
    */
   private val TTL_IN_SECONDS = 120 // Two minutes.

   /**
    * Choose of strategies for publishing or subscribing for nearby messages.
    */
   private val PUB_SUB_STRATEGY = Strategy.Builder().setTtlSeconds(TTL_IN_SECONDS).build()

   /**
    * The [Message] object used to broadcast information about the device to nearby devices.
    */
   private lateinit var message: Message

   /**
    * A [MessageListener] for processing messages from nearby devices.
    */
   private lateinit var messageListener: MessageListener

   /**
    * For adding [messages][Message] to the [RecyclerView]
    */
   private lateinit var msgAdapter: MessageAdapter

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

       binding.subscribeSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
           if (isChecked) {
               subscribe()
           } else {
               unsubscribe()
           }
       }

       binding.publishSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
           if (isChecked) {
               publish()
           } else {
               unpublish()
           }
       }
       setupMessagesDisplay()

       // The message being published is simply the Build.MODEL of the device. But since the
       // Messages API is expecting a byte array, you must convert the data to a byte array.
       message = Message(Build.MODEL.toByteArray(Charset.forName("UTF-8")))

       messageListener = object : MessageListener() {
           override fun onFound(message: Message) {
               // Called when a new message is found.
               val msgBody = String(message.content)
               msgAdapter.addItem(msgBody)
           }

           override fun onLost(message: Message) {
               // Called when a message is no longer detectable nearby.
               val msgBody = String(message.content)
               msgAdapter.removeItem(msgBody)
           }
       }

   }

   override fun onDestroy() {
       super.onDestroy()
       // although the API should shutdown its processes when the client process dies,
       // you may want to stop subscribing (and publishing if convenient)
       Nearby.getMessagesClient(this).unpublish(message)
       Nearby.getMessagesClient(this).unsubscribe(messageListener)
   }

   private fun publish() {
       val options = PublishOptions.Builder()
           .setStrategy(PUB_SUB_STRATEGY)
           .setCallback(object : PublishCallback() {
               override fun onExpired() {
                   super.onExpired()
                   // flick the switch off since the publishing has expired.
                   // recall that we had set expiration time to 120 seconds
                   runOnUiThread {
                       binding.publishSwitch.isChecked = false
                   }
                   runOnUiThread() {
                       binding.publishSwitch.isChecked = false
                   }
               }
           }).build()

       Nearby.getMessagesClient(this).publish(message, options)
   }

   private fun unpublish() {
       Nearby.getMessagesClient(this).unpublish(message)
   }

   private fun subscribe() {
       val options = SubscribeOptions.Builder()
           .setStrategy(PUB_SUB_STRATEGY)
           .setCallback(object : SubscribeCallback() {
               override fun onExpired() {
                   super.onExpired()
                   runOnUiThread {
                       binding.subscribeSwitch.isChecked = false
                   }
               }
           }).build()

       Nearby.getMessagesClient(this).subscribe(messageListener, options)
   }

   private fun unsubscribe() {
       Nearby.getMessagesClient(this).unsubscribe(messageListener)
   }

   private fun setupMessagesDisplay() {
       msgAdapter = MessageAdapter()
       with(binding.nearbyMsgRecyclerView) {
           layoutManager = LinearLayoutManager(context)
           this.adapter = msgAdapter
       }
   }

   class MessageAdapter : RecyclerView.Adapter<MessageAdapter.MessageVH>() {
       private var itemsList: MutableList<String> = arrayListOf()

       override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageVH {
           return MessageVH(TextView(parent.context))
       }

       override fun onBindViewHolder(holder: MessageVH, position: Int) {
           holder.bind(getItem(position))
       }

       override fun getItemCount(): Int = itemsList.size

       private fun getItem(pos: Int): String? = if (itemsList.isEmpty()) null else itemsList[pos]

       fun addItem(item: String) {
           itemsList.add(item)
           notifyItemInserted(itemsList.size)
       }

       fun removeItem(item: String) {
           val pos = itemsList.indexOf(item)
           itemsList.remove(item)
           notifyItemRemoved(pos)
       }

       inner class MessageVH(private val tv: TextView) : RecyclerView.ViewHolder(tv) {
           fun bind(item: String?) {
               item?.let { tv.text = it }
           }
       }
   }
}

Cómo agregar una clave de API de Google a tu archivo de manifiesto

La API de Nearby Messages tiene un componente de servidor que Google te proporciona. Cuando publicas un mensaje, la API de Nearby Messages envía el mensaje al servidor de Google, donde un suscriptor puede buscarlo. Para que Google reconozca tu app, debes agregar la API_KEY de Google al archivo Manifest.xml. Después de eso, podemos poner manos a la obra y jugar con nuestra app.

Obtener la clave de API es un proceso de tres pasos:

  1. Ve a Google Developers Console.
  2. Haz clic en Crear credenciales y elige Clave de API*.*
  3. Copia la clave de API creada y pégala en el archivo de manifiesto del proyecto de Android.

Agrega el elemento de metadatos com.google.android.nearby.messages.API_KEY dentro de la aplicación, en el archivo de manifiesto. El archivo debería parecerse a lo siguiente:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.example.nearbymessagesexample">

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.NearbyMessagesExample">

       <meta-data
           android:name="com.google.android.nearby.messages.API_KEY"
           android:value="ADD_KEY_HERE" />

       <activity android:name=".MainActivity">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

Una vez que hayas agregado la clave de API, ejecuta la app en dos o más dispositivos para ver cómo se comunican entre sí.

ba105a7c853704ac.gif

  • A fin de proteger la privacidad del usuario y preservar la duración de batería, debes detener la publicación y la suscripción en cuanto el usuario salga de la función cuando sea necesaria.
  • Debes usar Nearby Messages para establecer la proximidad entre dispositivos, pero no para la comunicación continua. La comunicación continua puede agotar las baterías del dispositivo a un ritmo de 2.5 a 3.5 veces mayor que el consumo normal.

¡Felicitaciones! Ahora sabes cómo enviar y descubrir mensajes entre dispositivos cercanos con la API de Nearby Messages.

En resumen, a fin de usar la API de Nearby Messages, debes agregar una dependencia para play-services-nearby, además de obtener una clave de API en Google Developers Console y agregarla a tu archivo Manifest.xml. La API requiere conexión a Internet para que los publicadores puedan enviar los mensajes al servidor de Google que sus suscriptores recibirán.

  • Aprendiste a enviar mensajes
  • Aprendiste a suscribirte para descubrir mensajes
  • Aprendiste a usar los mensajes (en este caso, simplemente los mostraste en una RecyclerView).

¿Qué sigue?

Consulta la serie de blogs y ejemplos