진행 중인 활동

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

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

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

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

하지만 알림을 보려면 사용자는 시계 화면 아래의 알림 트레이로 스와이프하여 올바른 알림을 찾아야 합니다. 이는 다른 표시 영역만큼 편리하지 않습니다.

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

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

달리기 아이콘

그림 1. 활동 표시기

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

런처

그림 2. 전역 런처

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

타이머

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

지도

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

음악

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

Wear는 미디어 앱에 관해 진행 중인 활동을 자동으로 생성합니다. 다른 종류의 앱에 관해 진행 중인 활동을 만드는 자세한 예는 GitHub의 진행 중인 활동 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 자체를 소통 메커니즘으로 활용합니다.

진행 중인 활동

진행 중인 알림을 받으면 진행 중인 활동을 간단하게 시작할 수 있습니다.

다음 코드 샘플에는 각 속성의 의미를 이해하는 데 도움이 되는 주석이 포함되어 있습니다.

Kotlin

var builder = 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)
        // In our case, 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())

자바

NotificationCompat.Builder builder = 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)
        // In our case, 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.TimerPart를 사용합니다. 그러면 앱 런처의 최근 섹션에서 스톱워치를 표현하기 위한 타이머 또는 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()

자바

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: 일반 텍스트 또는 크로노미터입니다. 앱 런처의 최근 섹션에 표시됩니다. 제공되지 않으면 '컨텍스트 텍스트' 알림이 사용됩니다.

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

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

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

진행 중인 활동 업데이트

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

앱이 백그라운드에서 실행 중이면 Ongoing Activity API에 업데이트를 보낼 수 있지만 빈도는 높지 않습니다. 업데이트 메서드는 시간적으로 서로 너무 가까운 호출을 무시할 수 있습니다. 분당 몇 번의 업데이트가 적절합니다.

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

Kotlin

ongoingActivity.update(context, newStatus)

자바

ongoingActivity.update(context, newStatus);

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

Kotlin

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

자바

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를 사용하여 진행 중인 활동과 연결합니다.