向应用添加“附近消息”功能

向应用添加“附近消息”功能

เกี่ยวกับ Codelab นี้

subjectอัปเดตล่าสุดเมื่อ ก.ย. 21, 2023
account_circleเขียนโดย Isai Damier

1 准备工作

假设您在拥挤的购物中心,看到了朋友小杰。您可能想挥舞手臂并大声呼喊来引起小杰的注意。借助 Google 的 Nearby Messages API,您可以让应用代用户完成大声呼喊来引起对方注意的环节,让朋友能在相距较近时轻松发现彼此。此 Codelab 教您如何使用 Nearby Messages API 实现近距离的用户互动。为简单起见,在此 Codelab 中,每个用户都将发布其手机的 build 型号:android.os.Build.MODEL。但实际上,您可以让每位用户向附近的朋友发布他们的 userId 或适合您的用例的其他信息。Nearby Messages API 结合使用互联网连接、蓝牙和其他技术来提供此功能。

前提条件

  • 具备 Kotlin 和 Android 开发方面的基础知识
  • 了解如何在 Android Studio 中创建和运行应用
  • 两台或多台用于运行和测试代码的 Android 设备

学习内容

  • 如何将 Nearby 库添加到应用
  • 如何向相关方广播消息
  • 如何检测来自相关方的消息
  • 如何为消息获取 API 密钥
  • 电池续航时间最佳做法

所需条件

  • 一个用于获取 Google API 密钥的 Google 账号(即 Gmail 地址)
  • 最新版本的 Android Studio
  • 两台安装有 Google Play 服务(即 Play 商店)的 Android 设备
  • 互联网连接(而不是不需要互联网连接的 Nearby Connections API)

构建内容

一个让用户可以发布设备信息并接收附近设备相关信息的 Activity 应用。此应用包含两个用户可以切换的开关:第一个开关用于发现或停止发现附近的消息;第二个开关用于发布或取消发布消息。对于此应用,我们希望发布和发现模式都在 120 秒后停止。为此,我们将稍微更深入地了解一下此 API,然后创建 PublishOptionsSubscribeOptions 对象,并利用这两者的 onExpired() 回调来关闭发布和订阅界面开关。

56bd91ffed49ec3d.png

2 创建 Android Studio 项目

  1. 启动一个新的 Android Studio 项目。
  2. 选择 Empty Activity

f2936f15aa940a21.png

  1. 将项目命名为 Nearby Messages Example,并将语言设置为 Kotlin。

3220c65e598bf6af.png

3 设置代码

  1. 最新版本的 Nearby 依赖项添加到您的应用级 build.gradle 文件中。这样您就可以使用 Nearby Messages API 来发送消息和检测来自附近设备的消息。
implementation 'com.google.android.gms:play-services-nearby:18.0.0'
  1. 在 Android 代码块中将 viewBinding 构建选项设置为 true,以启用 ViewBinding
android {
   
...
   buildFeatures
{
       viewBinding
true
   
}
}
  1. 点击 Sync Now 或绿色锤子按钮,让 Android Studio 能登记这些 Gradle 更改。

57995716c771d511.png

  1. 添加“Discover nearby devices”和“Share device information”这两个切换开关,以及会包含设备列表的 RecycleView。在 activity_main.xml 文件中,将代码替换为以下内容。
<?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>

注意:在您自己的项目中,应将值(如 16dp)更改为资源(如 @dimen/activity_vertical_margin)。

4 向应用添加“附近消息”功能

定义变量

  1. 在主 activity (MainActivity.kt) 中,将以下代码段粘贴在 onCreate() 函数的上方来定义以下变量。
/**
* 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

由于我们希望自定义广播应持续的时间,所以定义了 Strategy。在此 Codelab 中,我们选择了 120 秒。如果您未指定 Strategy,此 API 会使用默认值。此外,即使我们在此 Codelab 中为发布和订阅使用同一 Strategy,这也不是必需操作。

  1. 更改您的 onCreate() 函数,以将 ViewBinding 对象传入 setContentView()。这会显示 activity_main.xml 布局文件的内容。
override fun onCreate(savedInstanceState: Bundle?) {
   
super.onCreate(savedInstanceState)
   binding
= ActivityMainBinding.inflate(layoutInflater)
   setContentView
(binding.root)
}

连接界面按钮

此应用会执行三项操作:轻点一下开关即可发布消息、轻点一下开关即可发现消息,以及在 RecyclerView 中显示消息。

  1. 我们希望用户发布消息和取消发布消息,以及发现(即订阅)消息。现在,为此创建名为 publish()unpublish()subscribe()unsubscribe() 的桩方法。我们会在后续步骤中创建实现。
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. 用户可以使用 activity 布局中添加的开关来发布消息或发现(即订阅)消息。连接两个 Switches 以调用我们在 onCreate() 函数末尾定义的方法。
   binding.subscribeSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
       
if (isChecked) {
           subscribe
()
       
} else {
           unsubscribe
()
       
}
   
}

   binding
.publishSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
       
if (isChecked) {
           publish
()
       
} else {
           unpublish
()
       
}
   
}
  1. 现在,您已添加用于发布和发现消息的界面代码,接下来要设置用于显示和移除消息的 RecyclerViewRecyclerView 会显示正在主动发布的消息。订阅者会监听消息。发现消息后,订阅者会将其添加到 RecyclerView;当消息消失(即发布者停止发布)后,订阅者会将其从 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. onCreate() 函数的末尾,添加对 setupMessagesDisplay() 函数的调用。
override fun onCreate(savedInstanceState: Bundle?) {
   
...
   setupMessagesDisplay
()
}

现在,界面全部设置完毕,您可以开始发布消息,让附近的其他设备发现。此时,您的应用应如下所示:

56bd91ffed49ec3d.png

添加发布和发现代码

  1. 如要发送消息,我们首先需要一个 Message 对象。考虑到这是演示,我们仅发送设备的型号。将下面这段代码添加到 onCreate() 函数中,用于创建要发送的消息。
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. 如要发布其他附近的设备能发现的 Message,只需调用 Nearby.getMessagesClient(activity).publish(message)。不过,我们建议您更进一步,构建您自己的 PublishOptions 对象;这样一来就可以指定自己的自定义 Strategy,并利用 PublishCallback(在已发布的消息失效时发出通知)。在以下代码中,我们创建了一个选项,以便我们可以在已发布的 TTL 失效后为用户关闭开关。然后,我们会在调用 publish() 时传入此选项。将您的 publish() 函数更新为以下内容。
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)
}

每当用户开启 publish 开关时,此代码就会运行。

  1. 发布操作需要 Message,而订阅操作需要 MessageListener。但是在这里,我们同样建议您构建一个 SubscribeOptions 对象,即使该 API 正常运行并不需要此对象也要构建。例如,通过构建自己的 SubscriptionOption,您可以指定发现模式的持续时间。

将以下 MessageListener 代码添加到 onCreate() 函数中。检测到消息时,监听器会将其添加至 RecyclerView 中。当消息消失时,监听器会将其从 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. 从技术上讲,订阅者不需要 TTL(或者可以将 TTL 设置为无穷大)。但在此 Codelab 中,我们希望发现模式在 120 秒后停止。因此,我们要构建自己的 SubscribeOptions,并使用其 onExpired() 回调来关闭订阅界面 Switch。使用以下这段代码更新您的订阅函数。
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. 请务必让您的用户能够关闭信息共享功能。这意味着让发布者能停止发布,并让订阅者能停止订阅。如要停止发布,发布者必须指定要停止发布的消息。因此,如果正在广播 10 条消息,可以停止发布 1 条,留下 9 条。
private fun unpublish() {
   
Nearby.getMessagesClient(this).unpublish(message)
}
  1. 一个应用可以同时发布多条消息,但一次只能存在一个 MessageListener;因此,退订更为通用。如要停止订阅,订阅者必须指定监听器。
private fun unsubscribe() {
   
Nearby.getMessagesClient(this).unsubscribe(messageListener)
}
  1. 顺便提一下,虽然此 API 应在客户端进程终止时关闭其进程,但您可能想在 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)
}

此时,我们的代码应该可以编译了。不过,由于缺少 API 密钥,应用还无法按预期运行。您的 MainActivity 看上去应如下所示。

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

将 Google 提供的 API 密钥添加到清单文件中

Nearby Messages API 包含 Google 为您提供的服务器组件。当您发布消息后,Nearby Messages API 实际上会将消息发送到 Google 服务器,然后订阅者可以从此服务器查询消息。为了让 Google 能识别您的应用,您需要将 Google 提供的 API_KEY 添加到 Manifest.xml 文件中。之后我们就可以运行和操作应用了。

获取 API 密钥的过程分为三个步骤:

  1. 前往 Google 开发者控制台
  2. 点击 + 创建凭据,然后选择 API 密钥*。*
  3. 复制已创建的 API 密钥并将其粘贴到您的 Android 项目的清单文件中。

在您的清单文件中,在 application 内添加 com.google.android.nearby.messages.API_KEY 元数据项。此文件内容应如下所示。

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

添加 API 密钥后,在两台或更多设备上运行此应用,以查看设备之间的通信情况。

ba105a7c853704ac.gif

5 电池续航时间最佳实践

  • 为了保护用户隐私并延长电池续航时间,您应该在用户离开包含必要功用的功能后立即停止发布和订阅。
  • 您应使用“附近消息”功能在设备之间建立近程关系,而不是用于持续通信。持续通信时设备的电池电量消耗速度是正常情况的 2.5 到 3.5 倍。

6 恭喜

恭喜!您现在已经了解如何使用 Nearby Messages API 在附近的设备之间发送和发现消息。

简言之,如要使用 Nearby Messages API,您需要为 play-services-nearby 添加依赖项,还需要从 Google 开发者控制台获取 API 密钥并将其添加到 Manifest.xml 文件中。此 API 需要连接互联网,以便发布者可以将他们的消息发送到 Google 服务器,供订阅者抓取。

  • 您已了解如何发送消息
  • 您已了解如何通过订阅发现消息
  • 您已了解如何使用消息(在本例中,只是在 RecyclerView 中显示了消息)

后续操作

查看我们的博客系列和示例