진행 중인 활동

Wear OS에서 진행 중인 활동을 진행 중인 알림과 페어링하면 이 알림이 Wear OS 사용자 인터페이스 내의 추가 노출 영역에 추가됩니다. 이를 통해 사용자가 장기 실행 활동에 더 많이 참여할 수 있습니다.

진행 중인 알림은 일반적으로 사용자가 활발하게 참여하거나 특정 방식으로 대기하면서 기기를 점유하는 백그라운드 작업이 있는 알림을 나타내는 데 사용됩니다.

예를 들어, Wear OS 사용자는 운동 앱을 사용하여 특정 활동에서 달리기를 기록한 다음 앱에서 벗어나 다른 작업을 시작할 수 있습니다. 사용자가 운동 앱에서 벗어나면 앱은 달리기에 관한 정보를 지속적으로 사용자에게 제공하기 위해 일부 백그라운드 작업과 연결된 진행 중인 알림으로 전환됩니다. 알림에서는 사용자에게 업데이트 내용은 물론, 탭하여 앱으로 다시 돌아갈 수 있는 간단한 방법을 제공합니다.

하지만 알림을 보려면 사용자가 시계 화면 아래의 알림 트레이로 스와이프하여 해당 알림을 찾아야 합니다. 다른 노출 영역에 비하면 불편한 방법입니다.

Ongoing Activity API를 사용하면 앱의 진행 중인 알림에서 Wear OS의 편리한 여러 새 노출 영역에 정보를 노출하여 사용자의 참여를 유지할 수 있습니다.

예를 들어, 이 운동 앱에서는 정보를 사용자의 시계 화면에서 탭할 수 있는 달리기 아이콘으로 표시할 수 있습니다.

달리기 아이콘

그림 1. 활동 표시기

전역 앱 런처의 최근 섹션에도 진행 중인 활동이 나열됩니다.

런처

그림 2. 전역 런처

다음은 진행 중인 활동과 연결된 진행 중인 알림을 사용하기에 좋은 상황입니다.

타이머

그림 3. 타이머: 자동으로 시간을 카운트다운하며 타이머가 일시중지 또는 중지되면 종료됩니다.

지도

그림 4. 내비게이션 세부 경로 안내: 목적지까지의 경로를 알려줍니다. 사용자가 목적지에 도착하거나 내비게이션을 중지하면 종료됩니다.

음악

그림 5. 미디어: 세션 내내 음악을 재생합니다. 사용자가 세션을 일시중지하면 즉시 종료됩니다.

Wear는 미디어 앱에 관해 진행 중인 활동을 자동으로 생성합니다.

다른 종류의 앱에 관해 진행 중인 활동을 만드는 자세한 예는 진행 중인 활동 Codelab을 참고하세요.

설정

앱에서 Ongoing Activity API를 사용하려면 앱의 build.gradle 파일에 다음 종속 항목을 추가합니다.

dependencies {
  implementation "androidx.wear:wear-ongoing:1.0.0"
  // Includes LocusIdCompat and new Notification categories for Ongoing Activity.
  implementation "androidx.core:core:1.6.0"
}

진행 중인 활동 시작

진행 중인 알림을 만들고 진행 중인 활동을 만들어 시작해 보세요.

진행 중인 알림 만들기

진행 중인 활동은 진행 중인 알림과 밀접한 관련이 있습니다. 이러한 메서드는 함께 작동하여 사용자가 적극적으로 참여하고 있는 작업 또는 특정 방식으로 대기하면서 기기를 점유하는 작업을 사용자에게 알립니다.

진행 중인 활동을 진행 중인 알림과 페어링해야 합니다. 진행 중인 활동을 알림에 연결하면 다음과 같은 다양한 이점이 있습니다.

  • 알림은 진행 중인 활동을 지원하지 않는 기기의 대체 방안입니다. 알림은 백그라운드에서 실행되는 동안 유일하게 앱에 표시되는 노출 영역입니다.
  • Android 11 이상에서는 앱이 추가 노출 영역에 진행 중인 활동으로 표시되면 Wear OS가 알림 트레이에서 알림을 숨깁니다.
  • 현재 구현에서는 Notification 자체를 소통 메커니즘으로 활용합니다.

Notification.Builder.setOngoing을 사용하여 진행 중인 알림을 만듭니다.

진행 중인 활동 시작

진행 중인 알림이 있으면 다음 샘플과 같이 진행 중인 활동을 만듭니다. 포함된 주석을 확인하여 각 속성의 동작을 파악하세요.

Kotlin

var notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
      …
      .setSmallIcon(..)
      .setOngoing(true)

val ongoingActivityStatus = Status.Builder()
    // Sets the text used across various surfaces.
    .addTemplate(mainText)
    .build()

val ongoingActivity =
    OngoingActivity.Builder(
        applicationContext, NOTIFICATION_ID, notificationBuilder
    )
        // Sets the animated icon that will appear on the watch face in
        // active mode.
        // If it isn't set, the watch face will use the static icon in
        // active mode.
        .setAnimatedIcon(R.drawable.ic_walk)
        // Sets the icon that will appear on the watch face in ambient mode.
        // Falls back to Notification's smallIcon if not set.
        // If neither is set, an Exception is thrown.
        .setStaticIcon(R.drawable.ic_walk)
        // Sets the tap/touch event so users can re-enter your app from the
        // other surfaces.
        // Falls back to Notification's contentIntent if not set.
        // If neither is set, an Exception is thrown.
        .setTouchIntent(activityPendingIntent)
        // Here, sets the text used for the Ongoing Activity (more
        // options are available for timers and stopwatches).
        .setStatus(ongoingActivityStatus)
        .build()

ongoingActivity.apply(applicationContext)

notificationManager.notify(NOTIFICATION_ID, builder.build())

Java

NotificationCompat.Builder notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
      …
      .setSmallIcon(..)
      .setOngoing(true);

OngoingActivityStatus ongoingActivityStatus = OngoingActivityStatus.Builder()
    // Sets the text used across various surfaces.
    .addTemplate(mainText)
    .build();

OngoingActivity ongoingActivity =
    OngoingActivity.Builder(
        applicationContext, NOTIFICATION_ID, notificationBuilder
    )
        // Sets the animated icon that will appear on the watch face in
        // active mode.
        // If it isn't set, the watch face will use the static icon in
        // active mode.
        .setAnimatedIcon(R.drawable.ic_walk)
        // Sets the icon that will appear on the watch face in ambient mode.
        // Falls back to Notification's smallIcon if not set.
        // If neither is set, an Exception is thrown.
        .setStaticIcon(R.drawable.ic_walk)
        // Sets the tap/touch event so users can re-enter your app from the
        // other surfaces.
        // Falls back to Notification's contentIntent if not set.
        // If neither is set, an Exception is thrown.
        .setTouchIntent(activityPendingIntent)
        // Here, sets the text used for the Ongoing Activity (more
        // options are available for timers and stopwatches).
        .setStatus(ongoingActivityStatus)
        .build();

ongoingActivity.apply(applicationContext);

notificationManager.notify(NOTIFICATION_ID, builder.build());

다음 단계에서는 이전 예에서 가장 중요한 부분을 설명합니다.

  1. NotificationCompat.Builder에서 .setOngoing(true)를 호출하고 선택적 필드를 설정합니다.

  2. OngoingActivityStatus 또는 다른 상태 옵션(다음 섹션에서 설명)을 만들어 텍스트를 나타냅니다.

  3. OngoingActivity를 만들고 알림 ID를 설정합니다.

  4. 컨텍스트와 함께 OngoingActivity에서 apply()를 호출합니다.

  5. notificationManager.notify()를 호출하고 진행 중인 활동에서 설정된 것과 동일한 알림 ID를 전달하여 서로 연결합니다.

상태

Status를 사용하면 런처의 최근 섹션과 같은 새 노출 영역에서 OngoingActivity의 현재 실시간 상태를 사용자에게 노출할 수 있습니다. 이 기능을 사용하려면 Status.Builder 서브클래스를 사용하세요.

대부분의 경우 앱 런처의 최근 섹션에 표시할 텍스트를 나타내는 템플릿만 추가하면 됩니다.

그런 다음 addTemplate() 메서드를 사용하고 텍스트의 동적 부분을 Status.Part로 지정하여 스팬과 함께 텍스트를 표시할 방법을 맞춤설정할 수 있습니다.

다음 예는 '시간'이라는 단어를 빨간색으로 표시하는 방법을 보여줍니다. 이 예에서는 Status.StopwatchPart를 사용하여 앱 런처의 최근 섹션에서 스톱워치를 나타냅니다.

Kotlin

val htmlStatus =
        "<p>The <font color=\"red\">time</font> on your current #type# is #time#.</p>"

val statusTemplate =
        Html.fromHtml(
                htmlStatus,
                Html.FROM_HTML_MODE_COMPACT
        )

// Creates a 5 minute timer.
// Note the use of SystemClock.elapsedRealtime(), not System.currentTimeMillis().
val runStartTime = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(5)

val status = new Status.Builder()
   .addTemplate(statusTemplate)
   .addPart("type", Status.TextPart("run"))
   .addPart("time", Status.StopwatchPart(runStartTime)
   .build()

Java

String htmlStatus =
        "<p>The <font color=\"red\">time</font> on your current #type# is #time#.</p>";

Spanned statusTemplate =
        Html.fromHtml(
                htmlStatus,
                Html.FROM_HTML_MODE_COMPACT
        );

// Creates a 5 minute timer.
// Note the use of SystemClock.elapsedRealtime(), not System.currentTimeMillis().
Long runStartTime = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(5);

Status status = new Status.Builder()
   .addTemplate(statusTemplate)
   .addPart("type", new Status.TextPart("run"))
   .addPart("time", new Status.StopwatchPart(runStartTime)
   .build();

템플릿의 일부를 참조하려면 이름을 #로 묶습니다. 출력에서 #를 생성하려면 템플릿에서 ##를 사용합니다.

이전 예에서는 HTMLCompat를 사용하여 템플릿에 전달할 CharSequence를 생성합니다. 이는 Spannable 객체를 수동으로 정의하는 것보다 더 쉽습니다.

추가 맞춤설정

Status 외에도 다음과 같은 방법으로 진행 중인 활동이나 알림을 맞춤설정할 수 있습니다. 그러나 이러한 맞춤설정은 OEM 구현에 따라 사용되지 않을 수도 있습니다.

진행 중인 알림

  • 카테고리 집합에 따라 진행 중인 활동의 우선순위가 결정됩니다.
    • CATEGORY_CALL: 수신 음성 통화나 영상 통화 또는 유사한 동기 통신 요청
    • CATEGORY_NAVIGATION: 지도 또는 내비게이션 세부 경로 안내
    • CATEGORY_TRANSPORT: 재생을 위한 미디어 전송 제어
    • CATEGORY_ALARM: 알람 또는 타이머
    • CATEGORY_WORKOUT: 운동(새 카테고리)
    • CATEGORY_LOCATION_SHARING: 임시 위치 공유(새 카테고리)
    • CATEGORY_STOPWATCH: 스톱워치(새 카테고리)

진행 중인 활동

  • 애니메이션 아이콘: 흑백 벡터로, 배경이 투명한 것이 좋습니다. 활성 모드에서 시계 화면에 표시됩니다. 애니메이션 아이콘이 제공되지 않으면 기본 알림 아이콘이 사용됩니다. 기본 알림 아이콘은 애플리케이션마다 다릅니다.

  • 정적 아이콘: 배경이 투명한 벡터 아이콘입니다. 대기 모드에서 시계 화면에 표시됩니다. 애니메이션 아이콘이 설정되지 않으면 활성 모드에서 정적 아이콘이 시계 화면에 사용됩니다. 정적 아이콘이 제공되지 않으면 알림 아이콘이 사용됩니다. 둘 다 설정되어 있지 않으면 예외가 발생합니다. 앱 런처는 여전히 앱 아이콘을 사용합니다.

  • OngoingActivityStatus: 일반 텍스트 또는 Chronometer입니다. 앱 런처의 최근 섹션에 표시됩니다. 제공되지 않으면 '컨텍스트 텍스트' 알림이 사용됩니다.

  • 터치 인텐트: 사용자가 진행 중인 활동 아이콘을 탭할 경우 관련 앱으로 다시 전환하는 데 사용되는 PendingIntent입니다. 시계 화면이나 런처 항목에 표시됩니다. 앱을 실행하는 데 사용된 원래 인텐트와 다를 수 있습니다. 제공되지 않으면 알림의 콘텐츠 인텐트가 사용됩니다. 둘 다 설정되어 있지 않으면 예외가 발생합니다.

  • LocusId: 진행 중인 활동에 해당하는 런처 바로가기를 할당하는 ID입니다. 활동이 진행되는 동안 런처의 최근 섹션에 표시됩니다. 제공되지 않으면 런처는 최근 섹션의 모든 앱 항목을 같은 패키지에서 숨기고 진행 중인 활동만 표시합니다.

  • 진행 중인 활동 ID: 애플리케이션에 진행 중인 활동이 2개 이상 있는 경우 fromExistingOngoingActivity() 호출을 명확하게 구분하는 데 사용되는 ID입니다.

진행 중인 활동 업데이트

대부분의 경우 개발자는 화면의 데이터를 업데이트해야 할 경우 진행 중인 알림과 진행 중인 활동을 새로 만듭니다. 그러나 인스턴스를 다시 만드는 대신 유지하고자 하는 경우 Ongoing Activity API에서는 OngoingActivity를 업데이트하기 위한 도우미 메서드도 제공합니다.

앱이 백그라운드에서 실행 중이면 Ongoing Activity API에 업데이트를 보낼 수 있습니다. 그러나 업데이트 메서드는 서로 너무 가까운 호출을 무시하므로 이 작업을 너무 자주 실행하지는 마세요. 분당 몇 번의 업데이트가 적절합니다.

진행 중인 활동과 게시된 알림을 업데이트하려면 다음 예에서와 같이 이전에 만든 객체를 사용하고 update()를 호출합니다.

Kotlin

ongoingActivity.update(context, newStatus)

Java

ongoingActivity.update(context, newStatus);

편의상, 진행 중인 활동을 만드는 정적 메서드가 있습니다.

Kotlin

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

Java

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

진행 중인 활동 중지

앱이 진행 중인 활동으로서 실행 종료되면 진행 중인 알림만 취소하면 됩니다.

포그라운드로 전환될 때 알림 또는 진행 중인 활동을 취소하고 백그라운드로 다시 돌아갈 때 이를 다시 만들 수도 있지만 필수는 아닙니다.

진행 중인 활동 일시중지

앱에 명시적인 중지 작업이 있는 경우 일시중지가 해제되면 진행 중인 활동을 계속 진행합니다. 명시적인 중지 작업이 없는 앱의 경우 일시중지되면 활동을 종료합니다.

권장사항

Ongoing Activity API로 작업할 때는 다음 사항에 유의하세요.

  • notificationManager.notify(...)를 호출하기 전에 ongoingActivity.apply(context)를 호출합니다.
  • 진행 중인 활동의 정적 아이콘을 명시적으로 설정하거나 알림을 통해 대체 방안으로 설정합니다. 그러지 않으면 IllegalArgumentException이 발생합니다.

  • 배경이 투명한 흑백 벡터 아이콘을 사용합니다.

  • 진행 중인 활동의 터치 인텐트를 명시적으로 설정하거나 알림을 사용하여 대체 방안으로 설정합니다. 그러지 않으면 IllegalArgumentException이 발생합니다.

  • NotificationCompat의 경우 핵심 AndroidX 라이브러리 core:1.5.0-alpha05+를 사용합니다. 이 라이브러리에는 LocusIdCompat새로운 카테고리(운동, 스톱워치, 위치 공유)가 포함되어 있습니다.

  • 매니페스트에 선언된 MAIN LAUNCHER 활동이 앱에 두 개 이상 있는 경우 동적 바로가기를 게시하고 이를 LocusId를 사용하여 진행 중인 활동과 연결합니다.

Wear OS 기기에서 미디어를 재생할 때 미디어 알림 게시

미디어 콘텐츠가 Wear OS 기기에서 재생되고 있다면 미디어 알림을 게시하세요. 이렇게 하면 시스템이 상응하는 진행 중인 활동을 생성할 수 있습니다.

Media3을 사용하는 경우 알림이 자동으로 게시됩니다. 수동으로 알림을 만드는 경우 MediaStyleNotificationHelper.MediaStyle를 사용해야 하고 이에 상응하는 MediaSession세션 활동이 채워져 있어야 합니다.