ユーザーは、ドキュメントの確認、メッセージのトリアージ、さまざまなアイテムの閲覧などのタスクを Android スマートフォンで開始し、その後、大画面のディスプレイ、物理キーボード、マルチ ウィンドウ ワークスペースを備えた Chromebook に移行したいと考えることがよくあります。
Android 17(API レベル 37)以降の続行機能により、Googlebook のタスクバーに、ユーザーが近くのスマートフォンで実行していたアクティビティのアプリアイコン(候補)が表示されます。ユーザーは、ワンクリックで Googlebook でアクティビティを再開できます。
クロスデバイス ハンドオフを実装すると、アプリでアクティビティ コンテキストを転送できるようになり、ユーザーは、スマートフォンで中断したところから、アダプティブ Android アプリまたはパソコンのブラウザでワークフローを再開できます。
ハンドオフ フローを選択する
クロスデバイス ハンドオフを有効にする前に、Googlebook で [続行] の候補が選択されたときに、スマートフォン アプリの各アクティビティをどのように開くかを決定します。
| 引き継ぎフロー | Googlebook でのターゲット設定 | 最適な用途 | Key API |
|---|---|---|---|
| アプリ間のハンドオフ | デスクトップ ウィンドウで実行されているネイティブ Android アプリ | Google Play での大画面とデスクトップ ウィンドウ表示向けに最適化されたアプリ | HandoffActivityData.Builder |
| ウェブ フォールバックによるアプリ間のハンドオフ | インストールされている場合はネイティブ Android アプリ、インストールされていない場合はデフォルトのデスクトップ ブラウザの URL | ネイティブのデスクトップ ウィンドウ エクスペリエンス向けに最適化されているが、Googlebook にアプリがインストールされていない場合に信頼性の高いブラウザ フォールバックを必要とするアプリ | setFallbackUri |
| ウェブへの直接ハンドオフ | デフォルトのパソコン用ブラウザの URL(Android アプリリンク用に設定されている場合は、ネイティブ アプリによってインターセプトされます) | ウェブ アプリケーションが主なデスクトップ エクスペリエンスとなるワークフロー | HandoffActivityData.createWebHandoff |
アクティビティの引き継ぎを有効にする
Continue On はデフォルトで無効になっており、アクティビティごとに構成されます。スマートフォンで送信アクティビティごとに次の手順を行います。
- アクティビティがハンドオフに必要な状態を読み込んだら、
setHandoffEnabledを呼び出してtrueを渡します。有効にすると、アクティビティがフォアグラウンドにあるときやバックグラウンドに移行するときに、システムがハンドオフ データをリクエストできるようになります。 onHandoffActivityDataRequestedをオーバーライドして、null 以外のHandoffActivityDataオブジェクトを返します。isHandoffEnabledをいつでもクエリして、アクティビティでハンドオフが有効になっているかどうかを確認できます。
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"
}
}
引き継ぎ用のパッケージ アクティビティの状態
システムは、次の 2 つのシナリオで onHandoffActivityDataRequested を呼び出します。
- フォアグラウンド ハンドオフ: アクティビティがスマートフォンのフォアグラウンドにあり、ユーザーが Googlebook のタスクバーからハンドオフを開始します。
- バックグラウンド状態のキャプチャ: アクティビティがスマートフォンのバックグラウンドに移動し、ユーザーが後で Googlebook の候補を選択した場合に備えて、システムがハンドオフ ペイロードをキャッシュに保存します。
ハンドオフ リクエストが有効かどうかを確認する
isActiveRequest を使用して HandoffActivityDataRequestInfo を検査し、アクティブなユーザー開始ハンドオフとバックグラウンド保存を区別します。
isActiveRequestがtrueを返した場合、アクティビティが表示されている間に、ユーザーが別のデバイスで続行することを積極的にリクエストしています。isActiveRequestがfalseを返した場合、システムはアクティビティの停止時に状態をキャッシュに保存しています。
タスクが Googlebook に移動していることを確認する視覚的インジケーターをスマートフォンに表示する場合は、onHandoffActivityDataRequested が遅延なく戻るように、UI の更新を非同期でスケジュールします。
状態エクストラとウェブ フォールバックをアタッチする
HandoffActivityData ペイロードには次の情報が含まれます。
- ターゲット コンポーネント(アプリ間のハンドオフに必要):
this.getComponentName()(アクティビティのComponentName)または明示的なComponentName(this, TargetActivity::class.java)をHandoffActivityData.Builderに渡して、Googlebook で起動するアクティビティを指定します。 - Extras(省略可): 識別子と UI 座標(開いているドキュメント ID、アクティブなタブキー、スクロール位置など)を含む
PersistableBundleをsetExtras()に渡します。バンドルは 50 KB 未満にする必要があります。 - フォールバック URI(省略可): Android アプリがインストールされていない場合に Googlebook がデフォルトのブラウザでタスクを開けるように、ウェブ
UriをsetFallbackUriに渡します。
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"
}
}
ウェブへの直接ハンドオフを設定する
Googlebook の主なデスクトップ エクスペリエンスがウェブ アプリケーションである場合は、setAllowHandoffWithoutPackageInstalled(true) を伴う HandoffActivityParams を setHandoffEnabled に渡し、HandoffActivityData.Builder を使用する代わりに HandoffActivityData.createWebHandoff を呼び出します。
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 タスクバーで [Continue On] の候補を選択すると、システムはデスクトップ ウィンドウでターゲット ComponentName を起動し、起動 Intent に PersistableBundle エクストラを付加します。
Googlebook では、デスクトップ ウィンドウとマルチペイン レイアウトを考慮して、着信ハンドオフを処理します。
- ハンドオフ エクストラを抽出する: 受け取り側アクティビティの
onCreateメソッドとonNewIntentメソッドの両方で、受信したIntentからエクストラを取得します。onNewIntentを処理すると、アクティビティがsingleTop起動モードを使用している場合や、既存のデスクトップ ウィンドウにルーティングしている場合に、ウィンドウがハンドオフされたアイテムに更新されます。 - マルチペイン レイアウトに状態を適応させる: スマートフォンの全画面詳細ビューを占有していた状態は、Googlebook のリストと詳細レイアウトまたはサポートペイン レイアウト内の選択されたアイテムにマッピングされることがよくあります。プライマリ リストの選択と詳細ペインの両方にデータを入力して、デスクトップ ウィンドウに完全なコンテキストが表示されるようにします。
- ウィンドウ サイズ変更時に復元された状態を保持する: 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 間の引き継ぎをテストする
実装をエンドツーエンドで確認するには:
- 両方のデバイスを準備する
- Android スマートフォン(Android 17 以上を搭載)と Googlebook(またはデスクトップ ウィンドウ モードで実行されているテストデバイス)の両方で、同じ Google アカウントにログインします。
- Bluetooth をオンにして、両方のデバイスを同じ Wi-Fi ネットワークに接続します。
- 両方のデバイスで、[設定] > [接続済みのデバイス] > [接続の詳細設定] > [複数デバイス間サービス] > [アクティビティを続ける] を開き、設定のプロンプトに沿って タスクを有効にします。環境の前提条件については、セットアップとテストをご覧ください。
- アプリをインストールする
- アプリ間のハンドオフの場合、スマートフォンと Googlebook の両方にビルドをインストールします。
- ウェブ フォールバックまたはウェブへの直接ハンドオフの場合、ビルドをスマートフォンにインストールし、Googlebook でアプリをアンインストールしたままにして、ブラウザが想定どおりの URL を開くことを確認できるようにします。
- ハンドオフをトリガーする
- スマートフォンでアプリを開き、ハンドオフを有効にするアクティビティに移動します(アプリ間のハンドオフの場合は
setHandoffEnabled(true, null)を呼び出し、Googlebook でアプリをアンインストールしてテストする場合はsetAllowHandoffWithoutPackageInstalled(true)でHandoffActivityParamsを渡します)。 - Googlebook のタスクバーの右側で、[Continue On] バッジ付きのアプリアイコンを探して選択します。
- スマートフォンでアプリを開き、ハンドオフを有効にするアクティビティに移動します(アプリ間のハンドオフの場合は
- デスクトップの復元を確認する
- スマートフォンで表示していたアイテムとスクロール位置で、アプリがデスクトップ ウィンドウ(またはブラウザタブ)で開くことを確認します。
- Googlebook でアプリ ウィンドウのサイズを変更し、復元されたコンテンツと選択がウィンドウ サイズクラスの切り替え後も保持されることを確認します。