应用基础知识

实现无缝的跨设备互动

扬声器

关于

演讲简介

随着用户在手机、穿戴式设备和音频外围设备之间切换,提供流畅的跨设备体验对于提高互动度至关重要。然而,依赖标准的网络连接方法往往会导致电池电量耗尽、延迟激增和用户会话中断。

本深入探究课程将介绍专为多设备通信打造的 Android API,这些 API 提供了内存和能耗高效的模式,可确保设备之间的切换即时、可靠且直观。

摘要

  • 配套设备管理器会根据设备是否在附近来唤醒应用,并在不耗尽电池电量的情况下管理连接,从而取代蓝牙外围设备的前台服务。Android 17 专门针对医疗设备和运动手环添加了新的配置文件。
  • Jetpack Telecom 可作为 VoIP 应用的系统通话协调器,在手表和车载设备等已连接的设备上运行,并可向后兼容至 Android 8。它还会通过 CallControlScope 同步静音和挂断操作。
  • 即将推出的 Jetpack Telecom 1.1.0 功能包括系统拨号器中的统一通话记录 (SDK 36.1+) 以及在受支持的运营商网络上自动进行 5G 网络切片。
  • “快速分享”SDK 将于明年推出,以便开发者能够直接在 Android 和 iOS 应用中嵌入本地端到端加密文件分享功能。“触碰分享”(随 Pixel 11 推出)甚至可以自动唤醒已关闭的接收应用。

资源

Gemini Notebook

主题

  • 跨设备
  • companion-device-manager
  • jetpack-telecom
  • 快速分享
  • 蓝牙
  • voip
  • 点对点

转写文稿

跨设备互动简介

  • 大家好!

  • 欢迎参加“实现顺畅的跨设备互动”课程。

  • 我是 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 应用。

跨设备 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 简介

  • 因此,为了彻底解决所有这些问题,我们构建了 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 机器人钥匙圈中。

  • 我做得对吗?

  • 谢谢。

代码

代码段

“配套应用概览”中 06:17 处的 XML 代码段
<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" />
“配套应用概览”中 06:34 处的 Kotlin 代码段
class BleForegroundService : Service() {
   override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
      …
      return START_STICKY
   }
}
“使用配套设备管理器实现”中 08:03 处的 XML 代码段
<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" />
“使用配套设备管理器实现”视频中 08:38 处的 Kotlin 代码段
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?) { … }
   }
)
“使用配套设备管理器实现”视频中 09:47 处的 Kotlin 代码段
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) { … }
}
“Jetpack Telecom 简介”中 18:29 处的 XML 代码段
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
“Jetpack Telecom 简介”中 18:36 处的 Kotlin 代码段
// Initialize CallsManager & register supported capabilities once
val callsManager = CallsManager(context)

callsManager.registerAppWithTelecom(
    capabilities = CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING or
                   CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING
)
“Jetpack Telecom 简介”中 18:55 处的 Kotlin 代码段
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
}
“Jetpack Telecom 简介”中 19:49 处的 Kotlin 代码段
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)
}
“快速分享概览”中 29:31 处的 Kotlin 代码段
// 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)
“快速分享概览”中 29:57 处的 Kotlin 代码段
// 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)
“快速分享概览”中 30:23 处的 Kotlin 代码段
// 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)
“快速分享概览”中 30:49 处的 Kotlin 代码段
// 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")
    }
  }
}