백그라운드에서 데이터 모니터링

수동적 데이터 업데이트는 백그라운드에서 건강 관리 서비스 데이터를 모니터링해야 하는 앱에 적합합니다. 몇 시간이나 며칠 또는 그 이상의 시간에 걸친 사용 사례에 적합합니다. 앱이 실행되고 있지 않고 사용자가 명시적으로 운동하고 있지 않을 때 건강 데이터를 저장하거나 처리해야 하는 경우 건강 관리 서비스의 수동적 클라이언트를 사용합니다.

수동적 데이터 사용의 예는 다음을 참고하세요. 수동적 데이터수동적 목표 샘플이 있습니다.

종속 항목 추가

건강 관리 서비스에 종속 항목을 추가하려면 프로젝트에 Google Maven 저장소를 추가해야 합니다. 자세한 내용은 Google의 Maven 저장소를 참고하세요.

모듈 수준 build.gradle 파일에 다음 종속 항목을 추가합니다.

Groovy

dependencies {
    implementation "androidx.health:health-services-client:1.1.0-alpha03"
}

Kotlin

dependencies {
    implementation("androidx.health:health-services-client:1.1.0-alpha03")
}

기능 확인

데이터 업데이트를 등록하기 전에 기기가 앱에서 필요한 데이터 유형을 제공할 수 있는지 확인해야 합니다. 기능을 확인하면 특정 기능을 사용 설정하거나 중지할 수 있고 사용할 수 없는 기능을 보완하도록 앱의 UI를 수정할 수 있습니다.

val healthClient = HealthServices.getClient(this /*context*/)
val passiveMonitoringClient = healthClient.passiveMonitoringClient
lifecycleScope.launchWhenCreated {
    val capabilities = passiveMonitoringClient.capabilities.await()
    // Supported types for passive data collection
    supportsHeartRate =
        DataType.HEART_RATE_BPM in capabilities.supportedDataTypesPassiveMonitoring
    // Supported types for PassiveGoals
    supportsStepsGoal =
        DataType.STEPS_DAILY in capabilities.supportedDataTypesPassiveGoals
}

수동적 데이터 등록

서비스나 콜백(또는 둘 다)을 통해 수동적 데이터를 수신할 수 있습니다. 서비스를 사용하면 포그라운드에 앱이 조금도 표시되지 않는 경우 앱이 백그라운드에서 데이터를 수신할 수 있습니다. 백그라운드에서 데이터를 수신하면 데이터는 일괄적으로 전송됩니다. 콜백은 약간 더 빠른 속도로 데이터를 수신합니다. 하지만 앱이 실행 중이고 콜백이 성공적으로 알림을 수신한 경우에만 데이터를 수신합니다.

어떤 방법을 사용하든 다음 예와 같이 수신할 데이터 유형을 결정하는 PassiveListenerConfig를 먼저 만듭니다.

val passiveListenerConfig = PassiveListenerConfig.builder()
    .setDataTypes(setOf(DataType.HEART_RATE_BPM))
    .build()

콜백을 사용하여 데이터를 수신하려면 다음 예에서와 같이 콜백을 정의하고 등록합니다.

val passiveListenerCallback: PassiveListenerCallback = object : PassiveListenerCallback {
    override fun onNewDataPointsReceived(dataPoints: DataPointContainer) {
        // TODO: Do something with dataPoints
    }
}

passiveMonitoringClient.setPassiveListenerCallback(
    passiveListenerConfig,
    passiveListenerCallback
)

// To remove the listener
passiveMonitoringClient.clearPassiveListenerCallbackAsync()

서비스를 사용하는 것도 비슷하지만 다음 예에서와 같이 PassiveListenerCallback에서 파생된 클래스를 만드는 대신 PassiveListenerService에서 파생해야 합니다.

class PassiveDataService : PassiveListenerService() {
    override fun onNewDataPointsReceived(dataPoints: DataPointContainer) {
        // TODO: Do something with dataPoints
    }
}

passiveMonitoringClient.setPassiveListenerServiceAsync(
    PassiveDataService::class.java,
    passiveListenerConfig
)

그다음으로 AndroidManifest.xml 파일에서 서비스를 선언합니다. 건강 관리 서비스 권한을 요구합니다. 그래야 건강 관리 서비스만 서비스에 바인딩될 수 있습니다.

<service android:name=".PassiveDataService"
    android:permission="com.google.android.wearable.healthservices.permission.PASSIVE_DATA_BINDING"
    android:exported="true" />

시간 해석

건강 관리 서비스에서 수신되는 데이터는 일괄 처리됩니다. 따라서 서로 다른 유형의 데이터 포인트 또는 같은 유형의 여러 데이터 포인트를 같은 배치에서 수신할 수도 있습니다. 이벤트의 올바른 순서를 결정하려면 앱에서 수신한 시간이 아닌 그러한 객체에 포함된 타임스탬프를 사용합니다.

다음 예와 같이 부팅 타임스탬프를 먼저 계산하여 각 DataPoint의 타임스탬프를 가져옵니다.

val bootInstant =
    Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())

그런 다음 이 값을 getStartInstant() 또는 getEndInstant()에 전달할 수 있습니다.

부팅 후 등록 복원

수동 데이터 등록은 재부팅 시 유지되지 않습니다. 기기가 다시 시작된 후 데이터를 수신하려면 ACTION_BOOT_COMPLETED 시스템 브로드캐스트를 수신 대기하는 BroadcastReceiver를 사용하여 등록을 다시 만듭니다.

수신기에서 직접 등록을 복원하려고 하지 마세요. 대신 이 기능을 WorkManager 작업자에 위임하세요. 기기가 시작될 때 건강 관리 서비스가 수동적 데이터 등록 요청을 확인하는 데 10초 이상 걸릴 수 있으며 이는 허용되는 BroadcastReceiver 실행 시간을 초과할 수도 있습니다. 반면에 WorkManager 작업자에는 실행 제한이 10분 적용됩니다.

다음 스니펫은 BroadcastReceiver의 모양을 보여줍니다.

class StartupReceiver : BroadcastReceiver() {

   override fun onReceive(context: Context, intent: Intent) {
       if (intent.action != Intent.ACTION_BOOT_COMPLETED) return


       // TODO: Check permissions first
       WorkManager.getInstance(context).enqueue(
           OneTimeWorkRequestBuilder<RegisterForPassiveDataWorker>().build()
       )
   }
}

class RegisterForPassiveDataWorker(
   private val appContext: Context,
   workerParams: WorkerParameters
) : Worker(appContext, workerParams) {

   override fun doWork(): Result {
       runBlocking {
           HealthServices.getClient(appContext)
                .passiveMonitoringClient
                .setPassiveListenerCallback(...)
       }
       return Result.success()
   }
}

기기가 부팅될 때 시스템에서 이 코드를 실행하도록 처리하려면 AndroidManifest.xml 파일의 두 가지를 변경합니다.

먼저 <manifest>의 하위 요소로 다음 권한을 추가합니다.

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

둘째, 다음 수신기 인텐트 필터를 <application>의 하위 요소로 추가합니다.

<receiver
    android:name=".StartupReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

액티비티 상태

수동 클라이언트는 사용자의 수면 여부 등 사용자 상태에 관한 대략적인 정보를 제공할 수도 있습니다. 이러한 업데이트를 받으려면 다음 단계를 따르세요.

  1. ACTIVITY_RECOGNITION 권한을 요청합니다.
  2. PassiveListenerConfig 빌더에서 setShouldUserActivityInfoBeRequested(true)를 호출합니다.

다음 예와 같이 콜백 또는 서비스에서 onUserActivityInfoReceived() 메서드를 재정의하고 반환된 UserActivityInfo를 활용합니다.

override fun onUserActivityInfoReceived(info: UserActivityInfo) {
    val stateChangeTime: Instant = info.stateChangeTime // may be in the past!
    val userActivityState: UserActivityState = info.userActivityState
    if (userActivityState == UserActivityState.USER_ACTIVITY_ASLEEP) {
        // ...
    }
}

수동적 목표

수동적 목표(예: 사용자가 하루에 10,000보 걷기 달성)에 도달하면 앱에 알리도록 수동 클라이언트를 구성할 수 있습니다.

이렇게 하려면 다음 예에서와 같이 목표를 만듭니다.

val dailyStepsGoal by lazy {
    val condition = DataTypeCondition(
        dataType = DataType.STEPS_DAILY,
        threshold = 10_000, // Trigger every 10000 steps
        comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
    )
    PassiveGoal(condition)
}

다음 예와 같이 이 목표를 PassiveListenerConfig에 추가합니다.

val passiveListenerConfig = PassiveListenerConfig.builder()
    .setDailyGoals(setOf(dailyStepsGoal))
    .build()

다음 예와 같이 콜백 또는 서비스에서 onGoalCompleted() 메서드를 재정의하고 반환된 PassiveGoal을 활용합니다.

override fun onGoalCompleted(goal: PassiveGoal) {
    when (goal.dataTypeCondition.dataType) {
        DataType.STEPS_DAILY -> {
            // ...
        }
    }
}