支持在 Googlebook 上实现跨设备连续性

用户经常会在 Android 手机上开始执行任务,例如查看文档、处理消息或浏览各种商品,然后希望改用 Googlebook,因为 Googlebook 具有更大的显示屏、实体键盘和多窗口模式工作区。

在 Android 17(API 级别 37)及更高版本中,“继续操作”功能可让 Googlebook 任务栏显示用户在附近手机上运行的 activity 的应用图标(称为“建议”)。只需点击一下,用户即可在 Googlebook 上继续活动。

通过实现跨设备切换,您的应用可以转移 activity 上下文,以便用户在自适应 Android 应用或桌面浏览器中从手机上中断的地方继续工作流程。

选择移交流程

在启用跨设备切换之前,请确定当用户在 Googlebook 上选择“继续”建议时,手机应用中的每项活动应如何打开:

交接流程 Google 图书上的目标 适用场景 Key API
应用到应用的切换 在桌面窗口中运行的原生 Android 应用 Google Play 上针对大屏设备和桌面窗口化模式优化的应用 HandoffActivityData.Builder
应用到应用的切换(带有 Web 回退) 如果已安装,则为原生 Android 应用;否则,为默认桌面浏览器中的网址 针对原生桌面窗口体验进行了优化,但在 Googlebook 上未安装应用时需要可靠的浏览器回退的应用 setFallbackUri
直接切换到网络 默认桌面浏览器中的网址(或被原生应用拦截,如果配置了 Android 应用链接) Web 应用是主要桌面体验的工作流 HandoffActivityData.createWebHandoff

为 activity 启用移交

“继续操作”功能默认处于停用状态,并且是按 activity 配置的。在手机上执行每次发送活动时,请按以下步骤操作:

  1. 在 activity 加载了切换所需的必要状态后,调用 setHandoffEnabled 并传递 true。启用后,系统可以在 activity 处于前台或移至后台时请求切换数据。
  2. 替换 onHandoffActivityDataRequested 以返回非 null HandoffActivityData 对象。您可以随时查询 isHandoffEnabled,以检查 activity 是否处于切换状态。
import android.app.Activity
import android.app.HandoffActivityData
import android.app.HandoffActivityDataRequestInfo
import android.os.Bundle
import android.os.PersistableBundle

class DocumentEditorActivity : Activity() {

    private var documentId: String = "doc_42"
    private var scrollOffsetPx: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Enable handoff once the activity is ready to transfer state.
        setHandoffEnabled(true, null)
    }

    override fun onHandoffActivityDataRequested(
        handoffRequestInfo: HandoffActivityDataRequestInfo
    ): HandoffActivityData {
        val extras = PersistableBundle().apply {
            putString(EXTRA_DOCUMENT_ID, documentId)
            putInt(EXTRA_SCROLL_OFFSET, scrollOffsetPx)
        }

        return HandoffActivityData.Builder(getComponentName())
            .setExtras(extras)
            .build()
    }

    companion object {
        const val EXTRA_DOCUMENT_ID = "extra_document_id"
        const val EXTRA_SCROLL_OFFSET = "extra_scroll_offset"
    }
}

用于切换的软件包 activity 状态

系统会在以下两种情况下调用 onHandoffActivityDataRequested:

  • 前台切换:活动在手机上处于前台,用户从 Googlebook 任务栏发起切换。
  • 捕获后台状态:activity 移至手机后台,系统会缓存其切换有效负载,以防用户稍后在 Googlebook 上选择建议。

检查切换请求是否处于有效状态

使用 isActiveRequest 检查 HandoffActivityDataRequestInfo,以区分主动的用户发起的切换和后台保存:

  • 如果 isActiveRequest 返回 true,则表示用户已主动请求在 activity 可见时在另一设备上继续操作。
  • 如果 isActiveRequest 返回 false,则表示系统正在缓存状态,因为 activity 正在停止。

如果您在手机上显示一个视觉指示器来确认任务正在转移到 Googlebook,请异步安排界面更新,以便 onHandoffActivityDataRequested 立即返回。

附加状态 extra 和 Web 回退

HandoffActivityData 载荷包含以下信息:

  • 目标组件(应用到应用切换所需):传递 this.getComponentName()(activity 的 ComponentName)或显式 ComponentName(this, TargetActivity::class.java) 到 HandoffActivityData.Builder,以指定在 Googlebook 上启动哪个 activity。
  • 额外信息(可选):将包含标识符和界面坐标(例如,打开的文档 ID、有效标签页键或滚动位置)的 PersistableBundle 传递给 setExtras()。该软件包必须小于 50 KB。
  • 回退 URI(可选):将 Web Uri 传递给 setFallbackUri,以便在未安装 Android 应用的情况下,Googlebook 可以在默认浏览器中打开任务。
import android.app.Activity
import android.app.HandoffActivityData
import android.app.HandoffActivityDataRequestInfo
import android.app.HandoffActivityParams
import android.net.Uri
import android.os.Bundle
import android.os.PersistableBundle

class ArticleReaderActivity : Activity() {

    private var articleId: String = "android-desktop-windowing"
    private var readingProgressPercent: Int = 45

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setHandoffEnabled(
            true,
            HandoffActivityParams.Builder()
                .setAllowHandoffWithoutPackageInstalled(true)
                .build()
        )
    }

    override fun onHandoffActivityDataRequested(
        handoffRequestInfo: HandoffActivityDataRequestInfo
    ): HandoffActivityData {
        if (handoffRequestInfo.isActiveRequest()) {
            window.decorView.post {
                onHandoffStartedToSecondaryDevice()
            }
        }

        val extras = PersistableBundle().apply {
            putString(EXTRA_ARTICLE_ID, articleId)
            putInt(EXTRA_READING_PROGRESS, readingProgressPercent)
        }

        val fallbackWebUri = Uri.Builder()
            .scheme("https")
            .authority("example.com")
            .appendPath("articles")
            .appendPath(articleId)
            .appendQueryParameter("progress", readingProgressPercent.toString())
            .build()

        return HandoffActivityData.Builder(getComponentName())
            .setExtras(extras)
            .setFallbackUri(fallbackWebUri)
            .build()
    }

    private fun onHandoffStartedToSecondaryDevice() {
        // Save local drafts and update phone UI without blocking handoff.
    }

    companion object {
        const val EXTRA_ARTICLE_ID = "extra_article_id"
        const val EXTRA_READING_PROGRESS = "extra_reading_progress"
    }
}

配置直接到 Web 的切换

如果 Googlebook 上的主要桌面体验是 Web 应用,请将 HandoffActivityParams 传递给 setAllowHandoffWithoutPackageInstalled(true),然后传递给 setHandoffEnabled,并调用 HandoffActivityData.createWebHandoff,而不是使用 HandoffActivityData.Builder:

import android.app.Activity
import android.app.HandoffActivityData
import android.app.HandoffActivityDataRequestInfo
import android.app.HandoffActivityParams
import android.net.Uri
import android.os.Bundle

class WebDashboardActivity : Activity() {

    private var projectId: String = "proj_901"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setHandoffEnabled(
            true,
            HandoffActivityParams.Builder()
                .setAllowHandoffWithoutPackageInstalled(true)
                .build()
        )
    }

    override fun onHandoffActivityDataRequested(
        handoffRequestInfo: HandoffActivityDataRequestInfo
    ): HandoffActivityData {
        val webUri = Uri.Builder()
            .scheme("https")
            .authority("app.example.com")
            .appendPath("projects")
            .appendPath(projectId)
            .build()

        return HandoffActivityData.createWebHandoff(webUri)
    }
}

在 Googlebook 上接收和恢复状态

当用户在 Googlebook 任务栏中选择“继续”建议时,系统会在桌面窗口中启动目标 ComponentName,并将 PersistableBundle extra 附加到启动 Intent。

在 Googlebook 上,处理传入的切换时,请考虑窗口化模式和多窗格布局:

  1. 提取移交 extra:在接收 activity 的 onCreate 和 onNewIntent 方法中,从传入的 Intent 中检索 extra。处理 onNewIntent 可确保,如果您的 activity 使用 singleTop 启动模式或路由到现有桌面窗口,则该窗口会更新为移交的项。
  2. 使状态适应多窗格布局:在手机上占据全屏详情视图的状态通常会映射到 Googlebook 上列表-详情或支持窗格布局中的所选内容。填充主要列表选择和详情窗格,使桌面窗口具有完整的上下文。
  3. 在调整窗口大小时保留恢复的状态:将移交的状态存储在 Jetpack Compose 中的 ViewModel (SavedStateHandle) 或 rememberSaveable 中,以便在 Googlebook 上自由调整窗口大小时不会重置恢复的会话。
import android.app.Activity
import android.content.Intent
import android.os.Bundle

class DocumentEditorReceiverActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Restore from the handoff intent only on initial launch so subsequent
        // desktop window configuration changes use saved instance state.
        if (savedInstanceState == null) {
            handleHandoffIntent(intent)
        }
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        handleHandoffIntent(intent)
    }

    private fun handleHandoffIntent(incomingIntent: Intent?) {
        val documentId = incomingIntent?.getStringExtra(EXTRA_DOCUMENT_ID)
            ?: return
        val scrollOffsetPx = incomingIntent.getIntExtra(EXTRA_SCROLL_OFFSET, 0)

        openDocumentInDesktopWorkspace(
            documentId = documentId,
            initialScrollOffsetPx = scrollOffsetPx
        )
    }

    private fun openDocumentInDesktopWorkspace(
        documentId: String,
        initialScrollOffsetPx: Int
    ) {
        // Load the document into the list-detail layout and restore scroll position.
    }

    companion object {
        const val EXTRA_DOCUMENT_ID = "extra_document_id"
        const val EXTRA_SCROLL_OFFSET = "extra_scroll_offset"
    }
}

测试手机和 Googlebook 之间的 Handoff

如需端到端验证您的实现,请执行以下操作:

  1. 准备好两部设备
    • 在 Android 手机(运行 Android 17 或更高版本)和 Googlebook(或以窗口化模式运行的测试设备)上登录同一个 Google 账号。
    • 开启蓝牙,并将两部设备连接到同一 WLAN 网络。
    • 在两部设备上,依次打开“设置”>“已连接的设备”>“连接偏好设置”>“跨设备服务”>“继续活动”,按照设置提示操作,然后启用任务。如需了解完整的环境前提条件,请参阅设置和测试。
  2. 安装应用
    • 对于应用到应用切换,请在手机和 Googlebook 上都安装 build。
    • 对于网页回退或直接转到网页,请在手机上安装 build,并在 Googlebook 上保持应用处于未安装状态,以便验证浏览器是否会打开预期网址。
  3. 触发切换
    • 在手机上,打开应用并前往可实现切换的 activity(对于应用到应用的切换,调用 setHandoffEnabled(true, null);对于在 Googlebook 上测试已卸载的应用,传递 HandoffActivityParams 和 setAllowHandoffWithoutPackageInstalled(true))。
    • 在 Googlebook 上,查看任务栏右侧是否有带有“继续”标记的应用图标,然后选择该图标。
  4. 验证桌面恢复
    • 确认应用在桌面窗口(或浏览器标签页)中打开时,会显示手机上的确切商品和滚动位置。
    • 在 Googlebook 上调整应用窗口大小,并验证恢复的内容和所选内容在窗口大小类别转换期间是否保持不变。

其他资源