开发者关系工程师
随着用户在手机、穿戴式设备和音频外围设备之间切换,提供流畅的跨设备体验对于提高互动度至关重要。然而,依赖标准的网络连接方法往往会导致电池电量耗尽、延迟激增和用户会话中断。
本深入探究课程将介绍专为多设备通信打造的 Android API,这些 API 提供了内存和能耗高效的模式,可确保设备之间的切换即时、可靠且直观。
转写文稿
大家好!
欢迎参加“实现顺畅的跨设备互动”课程。
我是 Hanrui,今天我的同事 Yacine 和 Nataraj 也将参与本次讲座。
当我们谈到连接性时,通常会想到基本的网络链接或蓝牙耳机。
在 Android 上,连接性要广泛得多。
其核心在于让设备之间实现更高效的协作。
今天,我们将介绍常见的跨设备场景,并向您展示每个场景适用的 API。
在很长一段时间内,我们习惯于仅针对手机这一种设备构建应用。
随后,穿戴式设备开始兴起。
很快,智能手表与手机配对、在后台同步数据或直接接听电话就变得非常普遍。
添加耳…添加无线耳机。
现在,用户可以在通话过程中切换音频设备,从手表切换到手机、入耳式耳机或汽车。
最近,许多用户还会随身携带第二台大屏设备,例如可折叠设备或平板电脑。
他们希望应用在这些设备上都能顺畅运行。
这种体验还延伸到了笔记本电脑和 Chromebook,用户可以在办公桌前运行 Android 应用并保持连接。
早在 2023 年,平均每位用户就拥有 3 到 4 台设备。
用户希望这些设备不仅仅提供独立功能。
他们希望获得非凡的搭配使用体验。
当人们身处同一房间(就像我们今天这样),想要轻松地与身边的人分享照片、链接或应用时,这种“多设备协同体验”尤为明显。
从数据来看,这个生态系统规模庞大,且正在快速增长。
今年,全球物联网设备数量约为 220 亿,同比增长 14%。
在所有这些端点中,Wi-Fi 占三分之一,蓝牙占四分之一。
跨设备连接不再是边缘用例。
它已经成为我们日常生活的一部分。
然而,如今大多数应用仍基于低级网络和蓝牙 API 构建跨设备功能,这带来了三大挑战。
首先是电池和内存。
让后台扫描和服务持续运行会消耗电池电量和内存。
其次是延迟时间。
不可预测的延迟会干扰实时通话和数据同步。
第三,连接阻力。
在所有设备上管理发现、配对和重新连接非常复杂,很容易导致用户体验不佳。
为了消除这些烦恼,我们针对三大主要领域构建了专用 API,让跨设备功能变得非常简单。
我将首先介绍配套应用,然后由 Nataraj 介绍 VoIP 应用,最后由 Yacine 介绍 P2P 分享。
我们先来看看配套应用。
配套应用是指您在手机上安装的应用,可让智能手表或血糖传感器等配套设备与手机顺畅协作。
在本部分中,我们将重点介绍通过蓝牙连接的设备。
配套设备通常分为四类。
第一类是医疗设备,例如血糖传感器和生命体征监测仪。
以及智能手表和健身手环等穿戴式设备。
耳机,支持快速配对和智能控制。
以及车载套件,用于数字车钥匙和免触摸通话等功能。
每个类别都有自己的关键用户体验历程,但所有这些类别都依赖于设备与手机之间的可靠连接。
对于医疗设备,这种连接甚至可能关乎生命。
我们来看看这些类别中常见的四种需求。
首先是设备管理。
应用需要在附近的所有设备中找到正确的设备,例如这里的手表,而不是耳机或音箱,然后启动配对流程进行连接。
第二,到场检查。
应用需要知道设备何时在附近,何时不在附近。
当设备出现时,应用需要一种唤醒方式,以便在用户未打开应用的情况下也能执行其工作。
第三,连接维护。
连接建立后,应用希望连接可靠且持久。
如果连接中断(例如用户走出范围,然后又返回),应用需要自动重新连接,即使当时应用未运行也是如此。
第四,数据同步和传输。
这才是真正的价值所在。
同步传感器读数、推送固件更新或向设备发送设置。
对于医疗设备,这一点至关重要。
错过读数可能意味着错过警报。
所有这些都需要应用进程在适当的时间处于活跃和连接状态。
那么,应用如何满足所有这些需求?
过去,应用高度依赖前台服务。
以下是典型实现。
如果您之前曾构建过配套应用,可能会觉得这很熟悉。
您需要声明蓝牙扫描和连接权限、前台服务权限、已连接的设备的前台服务类型,然后声明通知权限,因为前台服务必须显示常驻通知。
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />然后,您运行一个粘性服务来使应用进程保持活跃状态。
class BleForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
…
return START_STICKY
}
}您知道,这种方法可行,但会消耗电池电量和内存,因为服务会一直运行,即使设备不在附近也是如此。
因此,我们构建了配套设备管理器(简称 CDM)。
首先,它可以根据设备的存在唤醒您的应用,只要设备保持在范围内,系统就会让您的应用进程保持活跃状态。
无需前台服务。
其次,CDM 支持设备配置文件。
借助设备配置文件,用户只需点按一下即可授予应用所需的所有权限,而无需通过一系列权限对话框逐一授予。
这大大提升了用户体验和转化率。
手表配置文件自 Android 12 起提供,眼镜配置文件自 Android 14 起提供。
在 Android 17 中,我们专门针对医疗设备和运动手环新增了两个配置文件。
目前,还有一些特权配置文件仅适用于系统应用。
如果我们从生态系统中了解到更广泛的需求,可能会考虑向更多开发者开放这些功能。
CDM 的使用非常简单。
我们以医疗设备为例。
首先,声明必要的权限:一项用于医疗设备,一项用于在后台运行,一项用于观察设备是否存在。
<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_MEDICAL" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />需要指出的是,与我们刚才看到的前台服务实现权限不同,此处列出的所有权限都是一般权限,这意味着它们将在安装时授予,不会显示提示对话框。
用户只需批准关联即可。
我们来看看如何创建这种关联。
在这里,我们使用设备过滤器构建关联请求,以便仅显示正确的设备。
val paringRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.setSingleDevice(true)
.setDeviceProfile(AssociationRequest.DEVICE_PROFILE_MEDICAL)
.build()
companionDeviceManager.associate(paringRequest, ContextCompat.getMainExecutor(this),
object : CompanionDeviceManager.Callback() {
override fun onAssociationPending(intentSender: IntentSender) {
intentSenderLauncher.launch(IntentSenderRequest.Builder(intentSender).build())
}
override fun onAssociationCreated(associationInfo: AssociationInfo) { … }
override fun onFailure(error: CharSequence?) { … }
}
)setSingleDevice 设置为 true,因为我们只需要一个匹配项,并将配置文件设置为 medical。
然后调用 associate。
CDM 负责处理扫描。
在 onAssociationPending 中,我们将获得一个 IntentSender。
启动该应用,系统会显示一个关联对话框。
有了医疗档案,只需点按一下,不仅能批准关联本身,还能授予档案所需的所有权限,甚至能绕过“勿扰”模式,这对于严重警报至关重要。
用户点按“允许”后,我们会收到 onAssociationCreated 和 AssociationInfo,其中包含一个关联 ID。
然后,我们可以通过使用该关联 ID 调用 startObservingDevicePresence 来观察设备在场情况。
此外,您还需要实现一项扩展 CompanionDeviceService 的服务。
class BleCompanionDeviceService : CompanionDeviceService() {
override fun onDevicePresenceEvent(event: DevicePresenceEvent) {
when (event.event) {
DevicePresenceEvent.EVENT_BLE_APPEARED -> onDeviceAppeared(event.associationId)
DevicePresenceEvent.EVENT_BLE_DISAPPEARED -> onDeviceDisappeared(event.associationId)
}
}
private fun onDeviceAppeared(associationId: Int) { … }
private fun onDeviceDisappeared(associationId: Int) { … }
}当设备进入范围时,系统会绑定到您的服务,即使您的应用未运行也是如此。
系统会使用 EVENT_BLE_APPEARED 调用 onDevicePresenceEvent。
然后,您就可以连接和同步了。
当设备离开时,您会收到 EVENT_BLE_DISAPPEARED。
然后,您可以执行清理。
没有始终开启的服务,也没有仅用于保持活动状态的通知。
不过,我们必须承认,CDM 确实存在一些限制,开发者必须牢记这些限制。
首先是操作系统版本。
大多数 CDM 功能都是在 Android 12 和 13 中引入的。
不过,Android 16 引入了一些很棒的改进,Android 17 也推出了新的配置文件。
因此,请在运行时检查操作系统版本,并为旧版本保留后备方案。
也就是说,目标 SDK 每年都会提高,最低 SDK 往往也会随之提高。
为了应对应用现在和未来面临的内存压力,我们强烈建议应用尽快从前台服务迁移到 CDM。
如果您有 CDM 目前不支持的用例,请告诉我们。
其次,目前 CDM 支持后台扫描,但不支持后台通告。
我们很少听到有这方面的需求。
不过,如果您的应用需要手机充当外围设备并在后台持续广播,我们很乐意了解更多相关信息。
第三,CDM 使用低功耗扫描模式。
它会牺牲一点实时响应能力,以换取电池续航时间和内存。
因此,请勿期望立即唤醒。
最后,不窥探载荷。
您无法注册特定特征或通告数据。
但一旦设备进入范围,您的应用就会被唤醒。
这样,它就可以根据需要连接并检查载荷。
以上就是关于配套应用和 CDM 的全部内容。
接下来,我将把时间交给 Nataraj,由他来介绍 VoIP 应用。
如果您的应用拥有专用外围设备并持续通过该设备进行通信,则 CompanionDeviceManager 是合适的工具。
大家好
我是 Nataraj K R。
接下来,我将介绍下一个跨设备场景,即实时通话。
如今,人们处理来电的方式正在发生变化。
超过 40% 的通话是通过 VoIP 网络进行的。
Android 在其中发挥着重要作用。
事实上,71% 的通话是在 Android 设备上进行的,几乎每隔一次通话就会发生这种情况。
不只是通话。
用户拥有的设备生态系统也在大幅增长。
全球有超过 2.5 亿辆汽车搭载 Android Auto,我们还向用户交付了超过 10 亿台蓝牙设备。
耳机、Wear OS 手表、Android、搭载 Android Auto 的汽车、蓝牙音箱,这些都是用户在手机之外拥有的设备,他们希望通话能在所有这些设备之间无缝切换。
例如,用户接到电话时,会看一眼手表,了解来电者信息。
他们会在手表上接听来电。
他们会根据需要将麦克风静音或取消静音,戴上耳机,继续通话。
如果他们想出门,就会进入车内,并希望通话能无缝切换到车内。
因此,这已成为用户的日常习惯,但并非优质体验。
对于用户来说,这是基本要求,而我们 Android 开发者有责任在 VoIP 应用中为用户提供这种顺畅的体验。
但如果您尝试从头开始自行构建所有这些内容,最终会遇到三大问题。
第一,蓝牙套接字的分片。
由于系统不知道您的应用正在处理通话,因此您最终需要构建自定义蓝牙套接字和自定义媒体会话,以便将通话投射到手表或 Android Auto。
第二个问题是音频焦点。
因为您必须处理低级 AudioManager API 和蓝牙 API,所以会发生音频泄漏,而这并不是系统的优先事项,对吧?
您的应用正在进行 VoIP 通话,因此每当有运营商来电时,通话就会中断,调制解调器会占用音频,导致您的应用无法播放音频。
更糟糕的是,即使蜂窝网络通话结束,音频焦点也不会返回到您的应用。
第三个问题是同步。
用户在手表上点按静音,但手机界面仍会显示通话未静音。
这会造成很大的困惑。
用户通过汽车转向柱挂断电话,但手机仍会显示正在通话的通知。
发生这种情况时,用户会感到非常沮丧,对吧?
因此,为了彻底解决所有这些问题,我们构建了 Jetpack Telecom。
因此,您只需使用 CallsManager 向 Jetpack Telecom 注册一次通话,Jetpack Telecom 就会充当中央系统通话协调器。
它负责将通话投射到手表、Android Auto 以及用户拥有的其他远程平台上。
它还会负责处理音频焦点。
最重要的是,所有这些功能都能无缝运行,甚至可以向后兼容到 Android 8。
将通话投影到手表或汽车屏幕上只是其中一部分,因为用户显然希望通过这些设备控制通话状态。
他们希望能够通过手表将通话静音,或根据需要取消静音。
他们希望通过汽车转向柱或点按两次耳机来挂断电话。
因此,Jetpack Telecom 会监听这些硬件事件,并将其作为回调集中呈现在一个位置,即 CallControlScope。
您只需监听这些回调,处理通话引擎并相应地更新通话状态。
Jetpack Telecom 负责在设备之间同步此通话状态。
因此,在后台,该库会提供两方面的优势。
首先,它显然能解决我们讨论过的所有问题。
因此,您无需处理低级 AudioManager API,该库会提供声明式调用端点,管理音频焦点,并在设备之间进行同步。
它负责在设备之间切换通话。
而且,每当有移动网络来电时,它也会妥善处理。
但该库带来的另一个好处是,由于该库基于 Kotlin 构建,因此每当您使用 Jetpack Telecom 添加通话时,它都会打开一个 Kotlin 协程作用域。
这样,它就可以利用 Kotlin 协程,并且该 Kotlin 协程会在调用期间保持活跃状态。
因此,通话一断开,您针对该通话运行的所有作业都会自动清理。
这样,您就不会留下任何孤立作业,并且调用生命周期也会由库来处理。
现在,您可能想知道这个出色的库非常棒,并且您想要实现它,对吗?
只需简单三步。
第 1 步,您显然应该声明依赖项。
然后,声明所需权限(即 MANAGE_OWN_CALLS),并实例化一个 CallsManager 对象。
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />将 CallsManager 对象实例化后,为应用注册您希望其提供的功能,例如视频通话或通话串流。
// Initialize CallsManager & register supported capabilities once
val callsManager = CallsManager(context)
callsManager.registerAppWithTelecom(
capabilities = CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING or
CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
)然后是第 2 步。
现在,您已向 Jetpack Telecom 注册应用,接下来需要处理来电和去电。
为此,您需要构建一个 CallAttributesCompat 对象,其中包含通话的元数据,例如主叫方或被叫方的 displayName、地址(即主叫方端点)、方向(指定通话是来电还是去电,以便 Jetpack Telecom 可以在这些远程界面上显示相应的界面),以及 callType(指定通话是语音通话还是视频通话,以便您获取所需资源)。
val attributes = CallAttributesCompat(
displayName = "Kukk Nat",
address = Uri.parse("tel:+16505550199"),
direction = CallAttributesCompat.DIRECTION_INCOMING,
callType = CallAttributesCompat.CALL_TYPE_AUDIO_CALL
)
callsManager.addCall(
attributes, onAnswer, onDisconnect, onSetActive, onSetInactive
) {
// Coroutine CallControlScope active for the duration of the call
}准备好元数据对象后,使用 callsManager.addCall 方法将其添加。
这会打开一个通话控制协程作用域。
添加完通话后,接下来是第 3 步。
第 3 步是监听用户在这些远程界面上执行的操作。
因此,由于您有 CallControlScope,您只需收集 Kotlin flow 即可。
callsManager.addCall(attributes, onAnswer, onDisconnect, onSetActive, onSetInactive) {
// 1. React to hardware mute toggles from Watch, Car, or Headset
launch {
isMuted.collect { muted -> audioEngine.setMicMuted(muted) }
}
// 2. Observe available endpoints & request declarative switches
launch {
currentCallEndpoint.collect { endpoint -> updateUi(endpoint) }
}
requestEndpointChange(preferredBluetoothEndpoint)
}您需要收集 isMuted,以便在用户通过这些远程界面将通话静音时做出响应,并相应地更新通话状态。
收集 currentCallEndpoint,以了解当前通话正在哪个端点进行,以便相应地更新界面。
您可以在手机界面上显示特定设备。
每当用户想要从一个设备切换到另一个设备时,只需使用用户想要的任何首选端点调用 requestEndpointChange 方法即可。
就是这么简单。
完成这三个步骤后,您的应用就已集成 Jetpack Telecom,可以正常使用了。
但还不止这些。
在 Jetpack Telecom 1.1.0 中,我们带来了更多功能,例如统一通话记录,这意味着您的所有 VoIP 通话现在都会原生显示在系统拨号器中。
因此,这样做的好处是,用户可以直接从系统拨号器一键回拨到您的应用。
因此,您无需在所有应用中搜索特定应用。
他们只需通过自己习惯使用的系统拨号器发起回拨即可。
但对于某些用户,如果您需要,我们也会提供精细的控制选项。
如果您不希望系统记录某个特定通话,只需在 CallAttributesCompat 对象中将 isLogExcluded 标志设为 true 即可。
我之前提到的元数据对象,您只需将其设置为 true,系统就不会记录您的通话,也不会在系统拨号器中显示。
1.1.0 中将推出的第二项出色功能是 5G 网络切片。
因此,在受支持的运营商网络上,网络会为您提供优质网络切片,这意味着对于特定用例,运营商会为您提供一个延迟低且无其他干扰的网络切片,从而让您获得更好的质量和体验。
因此,在受支持的运营商网络上,您无需额外费心,甚至无需编写任何代码,即可直接使用此功能。
集成该库后,您的调用将利用这些网络切片,从而获得更低的延迟和更出色的性能。
但如果出于某种原因,您不希望使用此功能,也可以使用 CAPABILITY_OPT_OUT_OF_PREMIUM_NETWORK 标志停用此功能。
Jetpack Telecom 已被 Google Meet、WhatsApp、KakaoTalk 和 Zoho Arattai 等全球顶级通信应用采用。
现在,您需要将此库集成到您的应用中,为用户提供更顺畅、更优质的体验。
现在,我们已经介绍了实时通话功能,接下来,我想把时间交给我的优秀同事 Yacine,请他来介绍“触碰分享”功能。
谢谢。
好了,各位,我知道你们有点累了,不过别担心,这是最后一节。
因此,我将介绍点对点文件共享,但我们也可以通过点对点共享实现其他用例。
在整个移动生态系统中,我们发现每天有超过 140 亿张照片和视频在社交媒体和即时通讯应用中分享,这是一个巨大的数字。
大多数情况下,所有这些文件都是通过云端共享的,因此您需要先拍摄照片,然后通过您喜欢的服务上传照片,接着对方必须通过另一端以相同的方式下载照片。
当您尝试邀请某人或朋友加入社交网络,或邀请某人协作处理播放列表或文档时,通常需要输入他们的用户名或电子邮件,这在键盘上需要花费一些时间。
你可能会犯错,这其实不是很好的体验,尤其是当你们两个就坐在彼此旁边时,却还要通过云端发送,还要等待。
只是不太好。
如今,我们都拥有各种各样的智能手机、平板电脑、可折叠设备,并用它们拍摄了许多精彩的照片。
但当您通过某些社交媒体或即时通讯应用分享这些照片时,出于性能问题和存储方面的考虑,这些照片会被压缩,最终在您的高清屏幕上,它们会变得像素化或画质不佳。
这并非您希望在高端设备上获得的最佳体验。
第二点,文件现在其实相当大。
一张照片的大小很容易就达到 10 MB,5 分钟的视频大小约为 500 MB,因此上传和下载这些内容需要相当长的时间。
最后,在另一端,您必须等待该延迟时间。
有时,您可能在拥挤的公交车上,随着公交车的移动,网络延迟和覆盖范围可能不理想,导致文件传输中断。有时,由于延迟过长,可能会发生超时,导致文件传输取消,这会让用户感到非常沮丧。
因此,与其依赖云端来分享所有这些文件,并不得不等待所有这些文件,为什么不使用实际的本地网络,如蓝牙或 Wi-Fi?
但如果您尝试自己实现,显然需要处理蓝牙管理器、Wi-Fi 管理器,还要编写自己的协议,我想,并非每个人都愿意花时间做这些。
但实际上,Android 上已经有类似的功能。
它已经开箱即用,您可能已经听说过它,那就是“快速分享”。
Android 上的“快速分享”功能已在系统中内置了这种抽象机制,您只需与其他设备分享文件,而无需管理所有这些底层细节。
它已在您的分享表中提供,并且会在两台设备之间切换最适合您设备的协议,而且它适用于 Marshmallow 及更高版本,如今,这已经是一个相当庞大的受众群体了,对吧?
最后,您在侧边看到的熟悉的界面和用户体验,对您自己的用户非常有帮助。
因此,他们知道界面是什么,他们不必学习如何使用它,他们过去已经见过它。
但我们知道,开发者也希望掌控体验。
您了解自己的应用,也可能比我们更了解您的用户。
那么,为什么不直接在应用中添加“快速分享”功能呢?
因此,我们开始与开发者沟通,发现“快速分享”已经相当普及。
它适用于 Android、Chromebook、Google 图书(最近推出)以及 Windows,覆盖超过 30 亿台设备,规模庞大。
此外,Pixel 11 还支持“触碰分享”功能。
因此,您无需选择要分享到的设备,只需点按即可,借助 NFC,我们会直接为您完成可检测性。
因此,您可以选择一张照片,也可以选择一个文件,只需轻触两台设备,它们就会通过碰撞直接分享。
同样,我们也知道有些开发者希望掌控整个体验。
我们知道,这是让用户能够使用特定功能(在本例中为文件共享)的最佳方式。
那么,为什么不在应用中嵌入“快速分享”功能呢?
因此,明年我们将直接为您提供“快速分享”SDK。
您将能够直接在应用中使用它,并且它已内置安全功能,即两台设备之间的端到端加密。
由于传输是在这些本地网络之间进行的,因此您将获得全速传输,这不仅适用于您的家庭 Wi-Fi,而且在您和对方之间,系统会自动创建热点,您无需手动操作。
对于支持 WLAN 直连的设备,此功能已包含在内。
您用这款出色的相机拍摄的文件将保持最佳分辨率和细节,以便您在大屏幕上清晰地查看。
由于它依赖于本地网络,因此您可以在地铁中使用它,即使在最深的地方,您仍然可以通过网络与朋友分享。
在飞机上也是如此,即使您身处数千英尺的高空,也仍然可以分享这些文件。
最后,对于开发者而言,这可以节省大量数据成本,因为用户无需使用流量套餐,这在他们外出旅行时尤为重要。
而且,您无需在自己的服务器上维护这些内容。
去年,我们向开发者和用户提到,借助“快速分享”功能,他们将能够通过“隔空投送”与 iPhone 设备分享文件。
借助“快速分享”,我们也将为开发者提供同样的功能。
我们将推出“快速分享”iOS SDK,让拥有 iOS 应用的开发者能够跨平台分享文件,不受设备限制。
以上就是精彩的演示,现在我们快速看一下代码。
我们尽量让它简单。
显然,您需要发起一个 QuickShareClient,附加要分享的文件,然后启动会话。此时,您会看到一个漂亮的模态窗口,即底部动作条,用户可以在其中预览正在发送的文件。如果您认为内容敏感,还可以启用屏幕上显示的选项。
// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val attachment = FileAttachment(
fileName = "report.pdf",
size = 1_048_576L,
uri = fileUri,
mimeType = "application/pdf",
// Sensitive content will be masked in preview
isSensitiveContent = false,
)
val content = ShareableContent.withFiles(listOf(attachment))
val request = SharingRequest.withContent(content)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)当您想在应用中接收文件时,同样,您无需添加附件,会话启动后,您也无需处理任何相关事宜。
// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val request = SharingRequest.withoutContent()
val session = quickShareClient.startSharingSession(request, lifecycleOwner)
// Open the Quick Share overlay in receive mode
session.launchWindow(activity, contentSelectionIntent = null)到目前为止,您已经看到,双方的代码都只有 20 行。
我们知道,借助“快速分享”和“触碰分享”功能,只需触碰设备即可分享文件,这实际上也催生了一种新的应用场景。
您的应用可能会在另一台设备上关闭,但只要点按一下,文件传输完成后,您的应用就会自动打开,您会收到 URI,以便在设备端进行一些处理,或者直接在应用中向用户显示一些内容。
// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val urlAttachment = TextAttachment(
text = groupAddUrl,
type = TextAttachment.Type.URL,
)
val content = ShareableContent.withText(urlAttachment)
val appLink = AppLink("https://myapp.com/group/invite")
val request = SharingRequest.withContent(content, appLink)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)因此,您同样无需在接收设备上打开应用。
使用“触碰分享”功能,系统会自动打开该应用。
当然,如果您希望界面对文件传输做出响应,只需使用 Kotlin Flow,即可获取所有事件并进行处理。
// Final API may undergo further updates
scope.launch {
for (event in session.sharingEvents) {
when (event) {
is SharingEvent.SendComplete ->
println("Send finished")
is SharingEvent.ReceiveComplete ->
println("Received, files=${event.content?.files?.size}")
else -> println("other event")
}
}
}今天我们已经讲了很多内容,现在我来做个总结。
别担心,您会有几分钟的休息时间。
正如 Hanrui 之前所说,CompanionDeviceManager 是处理任何蓝牙互动、持续蓝牙互动的最佳 API。
如果您的应用支持任何 IP 语音通话,这不一定是其主要用例,例如,您的应用中可能包含支持客户端,用户可以通过该客户端致电支持团队。
在这种情况下,Jetpack Telecom 同样是在 Android 上处理网络电话的最佳方式。
最后,对于任何点对点文件分享或打开应用,正如我所提到的,通过触碰分享,“快速分享”就是解决方案。
谢谢大家。在大家离开之前,我想说一件事,我们稍后会在外面回答大家的问题。
如果您对我们介绍的所有用例有任何疑问,欢迎在咨询交流时间向我们提问。
当然,别忘了在社交媒体上分享您从演讲或其他活动中获得的任何心得。
我们有 #AndroidDevSummit。
同样,对我们来说,了解活动是否吸引人、大家是否真的喜欢活动,这一点非常重要。
这让我们知道,我们确实有一个想要再次见到的社区。
您还可以前往服务台领取社交徽章,并将其添加到您的 Android 机器人钥匙圈中。
我做得对吗?
谢谢。
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />class BleForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
…
return START_STICKY
}
}<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_MEDICAL" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />val paringRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.setSingleDevice(true)
.setDeviceProfile(AssociationRequest.DEVICE_PROFILE_MEDICAL)
.build()
companionDeviceManager.associate(paringRequest, ContextCompat.getMainExecutor(this),
object : CompanionDeviceManager.Callback() {
override fun onAssociationPending(intentSender: IntentSender) {
intentSenderLauncher.launch(IntentSenderRequest.Builder(intentSender).build())
}
override fun onAssociationCreated(associationInfo: AssociationInfo) { … }
override fun onFailure(error: CharSequence?) { … }
}
)class BleCompanionDeviceService : CompanionDeviceService() {
override fun onDevicePresenceEvent(event: DevicePresenceEvent) {
when (event.event) {
DevicePresenceEvent.EVENT_BLE_APPEARED -> onDeviceAppeared(event.associationId)
DevicePresenceEvent.EVENT_BLE_DISAPPEARED -> onDeviceDisappeared(event.associationId)
}
}
private fun onDeviceAppeared(associationId: Int) { … }
private fun onDeviceDisappeared(associationId: Int) { … }
}<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />// Initialize CallsManager & register supported capabilities once
val callsManager = CallsManager(context)
callsManager.registerAppWithTelecom(
capabilities = CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING or
CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
)val attributes = CallAttributesCompat(
displayName = "Kukk Nat",
address = Uri.parse("tel:+16505550199"),
direction = CallAttributesCompat.DIRECTION_INCOMING,
callType = CallAttributesCompat.CALL_TYPE_AUDIO_CALL
)
callsManager.addCall(
attributes, onAnswer, onDisconnect, onSetActive, onSetInactive
) {
// Coroutine CallControlScope active for the duration of the call
}callsManager.addCall(attributes, onAnswer, onDisconnect, onSetActive, onSetInactive) {
// 1. React to hardware mute toggles from Watch, Car, or Headset
launch {
isMuted.collect { muted -> audioEngine.setMicMuted(muted) }
}
// 2. Observe available endpoints & request declarative switches
launch {
currentCallEndpoint.collect { endpoint -> updateUi(endpoint) }
}
requestEndpointChange(preferredBluetoothEndpoint)
}// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val attachment = FileAttachment(
fileName = "report.pdf",
size = 1_048_576L,
uri = fileUri,
mimeType = "application/pdf",
// Sensitive content will be masked in preview
isSensitiveContent = false,
)
val content = ShareableContent.withFiles(listOf(attachment))
val request = SharingRequest.withContent(content)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val request = SharingRequest.withoutContent()
val session = quickShareClient.startSharingSession(request, lifecycleOwner)
// Open the Quick Share overlay in receive mode
session.launchWindow(activity, contentSelectionIntent = null)// Final API may undergo further updates
val quickShareClient = QuickShareClient.create(activity)
val urlAttachment = TextAttachment(
text = groupAddUrl,
type = TextAttachment.Type.URL,
)
val content = ShareableContent.withText(urlAttachment)
val appLink = AppLink("https://myapp.com/group/invite")
val request = SharingRequest.withContent(content, appLink)
val session = quickShareClient.startSharingSession(request, lifecycleOwner)// Final API may undergo further updates
scope.launch {
for (event in session.sharingEvents) {
when (event) {
is SharingEvent.SendComplete ->
println("Send finished")
is SharingEvent.ReceiveComplete ->
println("Received, files=${event.content?.files?.size}")
else -> println("other event")
}
}
}