進行中のアクティビティ

Wear OS デバイスは、ワークアウトのトラッキングなど、長時間実行されるエクスペリエンスによく使用されます。これはユーザー エクスペリエンス上の課題です。ユーザーがタスクを開始してからウォッチフェイスに移動した場合、どのようにしてタスクに戻ればよいのでしょうか?ランチャーを使用してアプリに戻ることは、特に移動中に困難であり、不要な摩擦が生じます。

解決策は、進行中の通知を OngoingActivity とペア設定することです。これにより、デバイスはユーザー インターフェース全体に長時間実行中のアクティビティに関する情報を表示できるようになり、ウォッチフェイスの下部にあるタップ可能なアイコンなどの機能が有効になります。これにより、ユーザーはバックグラウンド タスクを認識し、ワンタップでアプリに戻ることができます。

たとえば、このワークアウト アプリでは、タップ可能な実行中のアイコンとして情報がユーザーのウォッチフェイスに表示されます。

ランニング アイコン

図 1. アクティビティ インジケーター

進行中の通知は、グローバル アプリ ランチャーの [履歴] セクションにも情報を表示します。これにより、ユーザーはタスクのステータスを確認してアプリに再エンゲージするための便利な場所をもう 1 つ利用できるようになります。

ランチャー

図 2. グローバル ランチャー

進行中のアクティビティに関連付けられた進行中の通知を使用するのに適した状況を以下に示します。

タイマー

図 3. タイマー: アクティブに時間をカウントダウンし、タイマーが一時停止または停止されると終了します。

map

図 4. ターンバイターン ナビゲーション: 目的地までのルートをアナウンスします。ユーザーが目的地に到着するか、ナビゲーションを停止すると終了します。

音楽

図 5. メディア: セッション全体で音楽を再生します。ユーザーがセッションを一時停止した直後に終了します。

Wear は、メディアアプリ用に進行中のアクティビティを自動的に作成します。

他の種類のアプリ用に進行中のアクティビティを作成する詳細な例については、進行中のアクティビティに関する Codelab をご覧ください。

設定

アプリで Ongoing Activity API の使用を開始するには、アプリの build.gradle ファイルに次の依存関係を追加します。

dependencies {
  implementation "androidx.wear:wear-ongoing:1.1.0"
  implementation "androidx.core:core:1.17.0"
}

進行中のアクティビティを作成する

このプロセスには次の 3 つのステップが含まれます。

  1. 標準の NotificationCompat.Builder を作成し、継続中として構成します。
  2. OngoingActivity オブジェクトを作成して構成し、通知ビルダーを渡します。
  3. 進行中のアクティビティを通知ビルダーに適用し、結果の通知を投稿します。

通知を作成して構成する

まず、NotificationCompat.Builder を作成します。重要なステップは、setOngoing(true) を呼び出して進行中の通知としてマークすることです。この段階で、小さなアイコンやカテゴリなど、他の通知プロパティを設定することもできます。

// Create a PendingIntent to pass to the notification builder
val pendingIntent =
    PendingIntent.getActivity(
        this,
        0,
        Intent(this, AlwaysOnActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
        },
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
    )

val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
    .setContentTitle("Always On Service")
    .setContentText("Service is running in background")
    .setSmallIcon(R.drawable.animated_walk)
    // Category helps the system prioritize the ongoing activity
    .setCategory(NotificationCompat.CATEGORY_WORKOUT)
    .setContentIntent(pendingIntent)
    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
    .setOngoing(true) // Important!

OngoingActivity を作成する

次に、ビルダーを使用して OngoingActivity のインスタンスを作成します。OngoingActivity.Builder には、Context、通知 ID、前の手順で作成した NotificationCompat.Builder が必要です。

新しい UI サーフェスに表示されるキー プロパティを構成します。

  • アニメーション アイコンと静的アイコン: アクティブ モードとアンビエント モードのウォッチフェイスに表示されるアイコンを指定します。
  • タップ インテント: ユーザーが進行中のアクティビティのアイコンをタップしたときに、アプリに戻るための PendingIntent。前のステップで作成した pendingIndent を再利用できます。

val ongoingActivity =
    OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder)
        // Sets the icon that appears on the watch face in active mode.
        .setAnimatedIcon(R.drawable.animated_walk)
        // Sets the icon that appears on the watch face in ambient mode.
        .setStaticIcon(R.drawable.ic_walk)
        // Sets the tap target to bring the user back to the app.
        .setTouchIntent(pendingIntent)
        .build()

通知に適用して投稿する

最後のステップは、OngoingActivity を通知にリンクして投稿することです。ongoingActivity.apply() メソッドは、元の通知ビルダーを変更し、システムが追加のサーフェスに表示できるように必要なデータを追加します。適用したら、通常どおり通知をビルドして投稿できます。

// This call modifies notificationBuilder to include the ongoing activity data.
ongoingActivity.apply(applicationContext)

// Post the notification.
startForeground(NOTIFICATION_ID, notificationBuilder.build())

ランチャーに動的なステータス テキストを追加

上記のコードは、タップ可能なアイコンをウォッチフェイスに追加します。ランチャーの [最近] セクションで、よりリッチなリアルタイム更新を提供するには、Status オブジェクトを作成して OngoingActivity にアタッチします。カスタム Status を指定しない場合、システムはデフォルトで通知のコンテンツ テキスト(setContentText() を使用して設定)を使用します。

動的テキストを表示するには、Status.Builder を使用します。プレースホルダを含むテンプレート文字列を定義し、それらのプレースホルダを埋める Status.Part オブジェクトを指定できます。Status.Part は、ストップウォッチやタイマーなどの動的なものにできます。

次の例は、「[ストップウォッチ タイマー] で実行中」と表示するステータスを作成する方法を示しています。

// Define a template with placeholders for the activity type and the timer.
val statusTemplate = "#type# for #time#"

// Set the start time for a stopwatch.
// Use SystemClock.elapsedRealtime() for time-based parts.
val runStartTime = SystemClock.elapsedRealtime()

val ongoingActivityStatus = Status.Builder()
    // Sets the template string.
    .addTemplate(statusTemplate)
    // Fills the #type# placeholder with a static text part.
    .addPart("type", Status.TextPart("Run"))
    // Fills the #time# placeholder with a stopwatch part.
    .addPart("time", Status.StopwatchPart(runStartTime))
    .build()

最後に、OngoingActivity.BuildersetStatus() を呼び出して、この StatusOngoingActivity にリンクします。

val ongoingActivity =
    OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder)
        // ...
        // Add the status to the OngoingActivity.
        .setStatus(ongoingActivityStatus)
        .build()

その他のカスタマイズ

Status 以外にも、次の方法で進行中のアクティビティまたは通知をカスタマイズできます。ただし、OEM の実装によっては、こうしたカスタマイズが使用されない場合もあります。

進行中の通知

  • 設定したカテゴリによって、進行中のアクティビティの優先度が決まります。
    • CATEGORY_CALL: 音声通話またはビデオ通話の着信、あるいはそれに類する同期を行う通信リクエスト
    • CATEGORY_NAVIGATION: 地図またはターンバイターン ナビゲーション
    • CATEGORY_TRANSPORT: 再生のメディア トランスポート コントロール
    • CATEGORY_ALARM: アラームまたはタイマー
    • CATEGORY_WORKOUT: ワークアウト
    • CATEGORY_LOCATION_SHARING: 一時的な現在地の共有カテゴリ)
    • CATEGORY_STOPWATCH: ストップウォッチ

進行中のアクティビティ

  • アニメーション アイコン: 白黒のベクター(背景が透明であることが望ましい)。アクティブ モードのとき、ウォッチフェイスに表示されます。アニメーション アイコンが設定されていない場合、デフォルトの通知アイコンが使用されます。デフォルトの通知アイコンはアプリによって異なります。

  • 静的アイコン: 背景が透明なベクターアイコン。常に画面表示モードのときウォッチフェイスに表示されます。アニメーション アイコンが設定されていない場合、アクティブ モードのウォッチフェイスには静的アイコンが使用されます。静的アイコンが設定されていない場合、通知アイコンが使用されます。どちらも設定されていない場合は、例外がスローされます。(アプリ ランチャーには引き続きアプリアイコンが使用されます)。

  • OngoingActivityStatus: 書式なしテキストまたは Chronometer。アプリ ランチャーの履歴セクションに表示されます。設定されていない場合は、通知「コンテキスト テキスト」が使用されます。

  • タップ インテント: ユーザーが進行中のアクティビティのアイコンをタップしたとき、アプリに戻るために使用される PendingIntent。ウォッチフェイスまたはランチャー アイテムに表示されます。アプリの起動に使用された元のインテントとは異なる場合があります。設定されていない場合は、通知のコンテンツ インテントが使用されます。どちらも設定されていない場合は、例外がスローされます。

  • LocusId: 進行中のアクティビティに対応するランチャー ショートカットを割り当てる ID。アクティビティの進行中は、ランチャーの [履歴] セクションに表示されます。設定されていない場合、ランチャーは、同じパッケージの [履歴] セクションにあるすべてのアプリアイテムを非表示にし、進行中のアクティビティのみを表示します。

  • 進行中のアクティビティの ID: アプリに進行中のアクティビティが複数ある場合に、fromExistingOngoingActivity() の呼び出しを明確にするために使用される ID。

進行中のアクティビティを更新する

デベロッパーはほとんどの場合、画面上のデータを更新する必要があるとき、進行中の通知と進行中のアクティビティを新規作成します。しかし、インスタンスを再作成するのではなく保持する場合、Ongoing Activity API には OngoingActivity を更新するヘルパー メソッドも用意されています。

アプリがバックグラウンドで動作している場合、Ongoing Activity API に更新を送信できます。ただし、更新メソッドは互いに近すぎる呼び出しを無視するため、頻繁に実行しないでください。1 分間に数回の更新が妥当です。

進行中のアクティビティと送信された通知を更新するには、次のように、前に作成したオブジェクトを使用して update() を呼び出します。

ongoingActivity.update(context, newStatus)

便宜上、進行中のアクティビティを作成するための静的メソッドが用意されています。

OngoingActivity.recoverOngoingActivity(context)
               .update(context, newStatus)

進行中のアクティビティを停止する

アプリが進行中のアクティビティとしての動作を完了するときは、進行中の通知をキャンセルするだけで済みます。

フォアグラウンドになったときに通知または進行中のアクティビティをキャンセルし、バックグラウンドに戻ったときにそれらを再作成することもできますが、これは必須ではありません。

進行中のアクティビティを一時停止する

アプリに明示的な停止アクションがある場合は、一時停止を解除した後も進行中のアクティビティを継続します。明示的な停止アクションがないアプリの場合は、一時停止したときにアクティビティを終了します。

おすすめの方法

Ongoing Activity API を使用する場合は、次の点に注意してください。

  • 進行中のアクティビティの静的アイコンを明示的に設定するか、通知を使用したフォールバックとして設定してください。そうしなかった場合、IllegalArgumentException が発生します。

  • 背景が透明な白黒のベクターアイコンを使用してください。

  • 進行中のアクティビティのタップ インテントを明示的に設定するか、通知を使用したフォールバックとして設定してください。そうしなかった場合、IllegalArgumentException が発生します。

  • アプリのマニフェストで MAIN LAUNCHER アクティビティが複数宣言されている場合は、動的ショートカットを公開し、LocusId を使用して進行中のアクティビティに関連付けてください。

Wear OS デバイスでメディアを再生する際にメディア通知をパブリッシュする

メディア コンテンツが Wear OS デバイスで再生されている場合は、メディア通知をパブリッシュします。これにより、対応する進行中のアクティビティをシステムで作成できます。

Media3 を使用している場合、通知は自動的にパブリッシュされます。通知を手動で作成すると、MediaStyleNotificationHelper.MediaStyle が使用されます。それに対応する MediaSession にはセッション アクティビティが入力されているはずです。