Add Nearby Messages to your app

Imagine you are in a crowded mall and you spot your friend Jane. Your inclination might be to wave and shout as you try to get Jane's attention. Google's Nearby Messages API is here to enable your app to do the attention-grabbing shouting for your users, so that friends can easily discover each other when they are in close physical proximity. This codelab teaches you how to use the Nearby Messages API to enable user interactions based on physical proximity. To keep the codelab simple, each user will publish the build model of their phone: android.os.Build.MODEL. But in reality, you could have each user publish their userId, or other information suitable to your use case, to nearby friends. The Nearby Messages API combines internet connectivity, Bluetooth, and other technologies to deliver this feature.

Prerequisites

  • Basic knowledge of Kotlin and Android development
  • How to create and run apps on Android Studio
  • Two or more Android devices for running and testing the code

What you'll learn

  • How to add the Nearby library to your app
  • How to broadcast messages to interested parties
  • How to detect messages from points of interest
  • How to get an API Key for your messages
  • Best practices for battery life

What you'll need

  • A Google Account (i.e. Gmail address) for obtaining a Google API Key
  • The latest version of Android Studio
  • Two Android devices with Google Play Services (in other words, the Play Store) installed on them
  • An internet connection (As opposed to the Nearby Connections API that doesn't require it)

What you'll build

A single Activity app that allows a user to publish device information and receive information about nearby devices. The app has two switches that the user can toggle: the first switch is for discovering or stopping discovering nearby messages; the second switch is to publish or unpublish messages. For this app, we want both publishing and discovery to stop after an arbitrary 120 seconds. To that end, we will dig a little deeper into the API and create a PublishOptions and a SubscribeOptions object and use their onExpired() callbacks to turn off the publish and subscribe UI switches.

56bd91ffed49ec3d.png

  1. Start a new Android Studio project.
  2. Choose Empty Activity.

f2936f15aa940a21.png

  1. Name the project Nearby Messages Example and set the language to Kotlin.

3220c65e598bf6af.png

  1. Add the latest version of the Nearby dependency into your app-level build.gradle file. This allows you to use the Nearby Messages API to send and detect messages from nearby devices.
implementation 'com.google.android.gms:play-services-nearby:18.0.0'
  1. Set the viewBinding build option to true in the android block to enable ViewBinding.
android {
   ...
   buildFeatures {
       viewBinding true
   }
}
  1. Click Sync Now or the green hammer button to enable Android Studio to register these Gradle changes.

57995716c771d511.png

  1. Add the "Discover nearby devices" and "Share device information' toggles and the RecycleView that will contain the list of devices. In the activity_main.xml file, replace the code with the following.
<?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>

Note: In your own project, change values such as 16dp to resources such as @dimen/activity_vertical_margin.

Define variables

  1. Inside the main activity (MainActivity.kt), above the onCreate() function, define the following variables by pasting this code snippet.
/**
* 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

We defined a Strategy because we want to customize how long a broadcast should last. For this codelab, we choose 120 seconds. If you don't specify a strategy, the API uses the defaults for you. Also, even though we're using the same Strategy for both publishing and subscribing in this codelab, you aren't required to.

  1. Change your onCreate() function to pass in the ViewBinding object to setContentView(). This displays the contents of the activity_main.xml layout file.
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

Wire UI buttons

This app will do three things: publish messages at the flick of a switch, discover messages at the flick of a switch, and display the messages in a RecyclerView.

  1. We want the user to publish and unpublish messages and to discover (i.e. subscribe to) messages. For now, create stub methods for this, called publish(), unpublish(), subscribe(), unsubscribe(). We'll create the implementation in a future step.
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. The user can publish or discover (i.e. subscribe to) messages using the switches added to the activity's layout. Wire the two Switches to call the methods we defined at the end of the onCreate() function.
   binding.subscribeSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
       if (isChecked) {
           subscribe()
       } else {
           unsubscribe()
       }
   }

   binding.publishSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
       if (isChecked) {
           publish()
       } else {
           unpublish()
       }
   }
  1. Now that you've added the UI code for publishing and discovering messages, set up the RecyclerView for displaying and removing messages. The RecyclerView will display the messages that are being actively published. The subscriber will be listening for messages. When a message is found, the subscriber will add it to the RecyclerView; when a message is lost, i.e. the publisher stops publishing it, the subscriber will remove it from the 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. At the end of the onCreate()function, add a call to the setupMessagesDisplay()function.
override fun onCreate(savedInstanceState: Bundle?) {
   ...
   setupMessagesDisplay()
}

Now that the UI is all set up, we are ready to start publishing messages for other nearby devices to discover. Here is how your app should look at this point:

56bd91ffed49ec3d.png

Add publish and discovery code

  1. To send a message, we first need a Message object. Since this is a demo, we are simply sending the device's model. Add this code to the onCreate() function to create the message to be sent.
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. To publish a Message that other nearby devices can discover, you simply need to call Nearby.getMessagesClient(activity).publish(message). However, we recommend that you go a step further by building your own PublishOptions object; this lets you specify your own custom Strategy and take advantage of the PublishCallback, which notifies when a published message has expired. In the following code we create an option so that we can turn off the switch for the user when the published TTL expires. Then we pass in the option when we call publish(). Update your publish() function to the following.
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)
}

This code runs every time the user turns ON the publish switch.

  1. While publishing requires a Message, subscribing requires a MessageListener. But here too, we recommend that you build a SubscribeOptions object even though one isn't required for the API to work. Building your own SubscriptionOption allows you, for example, to specify how long you want to be in discovery mode.

Add the following MessageListener code to the onCreate() function. When a message is detected, the listener adds it to the RecyclerView. When a message is lost, the listener removes it from the 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. Technically a subscriber doesn't need a TTL (or rather can set their TTL to infinity). But in this codelab we want to stop discovery after 120 seconds. Consequently, we will build our own SubscribeOptions and use its onExpired() callback to turn off the subscribe UI Switch. Update your subscribe function with this code.
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. It's important to allow your users to turn off information sharing. This means allowing publishers to stop publishing, and allowing subscribers to stop subscribing. To stop publishing, a publisher must specify the message they want to stop publishing. Therefore, if you have ten messages being broadcast, you can stop publishing one, leaving nine.
private fun unpublish() {
   Nearby.getMessagesClient(this).unpublish(message)
}
  1. While an app can publish multiple messages at the same time, it can only have one MessageListener at a time; hence, unsubscribing is more general. To stop subscribing, a subscriber must specify the listener.
private fun unsubscribe() {
   Nearby.getMessagesClient(this).unsubscribe(messageListener)
}
  1. As a side note, although the API should shutdown its processes when the client process dies, you may want to stop subscribing (and publishing if convenient) in your onDestroy() lifecycle method.
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)
}

At this point our code should compile. But the app won't work as expected yet because it's missing the API key. Your MainActivity should look like this:

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 }
           }
       }
   }
}

Add an API Key from Google to your manifest file

The Nearby Messages API has a server component that Google provides for you. When you publish a message, the Nearby Messages API actually sends the message to the Google server, where a subscriber can then query for the message. In order for Google to recognize your app, you need to add the API_KEY from Google to the Manifest.xml file. After that we can just hit run and play with our app.

Getting the API Key is a three step process:

  1. Go to the Google Developer Console.
  2. Click on + Create Credentials and choose API Key*.*
  3. Copy the API key created and paste it into your Android project's manifest file.

Add the com.google.android.nearby.messages.API_KEY meta-data item inside the application in your manifest file. It file should look similar to this.

<?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>

Once you have added the API Key, run the app on two or more devices to see them communicate with each other.

ba105a7c853704ac.gif

  • To protect user privacy and to preserve battery life, you should stop publishing and subscribing as soon as the user leaves the feature where the functionality is necessary.
  • You should use Nearby Messages to establish proximity between devices, but not for continuous communication. Continuous communication can drain device batteries at a rate of 2.5 to 3.5 times the normal battery consumption rate.

Congratulations! You now know how to send and discover messages between nearby devices using the Nearby Messages API.

In summary, to use the Nearby Messages API you need to add dependency for play-services-nearby and you need to get an API Key from the Google Developer Console and add it to your Manifest.xml file. The API requires an internet connection so publishers can send their messages to the Google server for their subscribers to grab.

  • You learned how to send messages
  • You learned how to subscribe to discover messages
  • You learned how to use the messages (in this case you simply displayed them in a RecyclerView)

What's Next?

Check out our blog series and sample