Monitorar dados em segundo plano

As atualizações de dados passivos são recomendadas para apps que precisam monitorar dados dos Recursos de saúde em segundo plano. Elas são destinadas a casos de uso que abrangem horas, dias ou até períodos mais longos. Se você precisa armazenar ou processar dados de saúde quando o app não estiver em execução e o usuário não estiver fazendo exercício, use o cliente passivo dos Recursos de saúde.

Para ver exemplos de uso de dados passivos, consulte o Dados passivos e Metas passivas no GitHub.

Adicionar dependências

Para adicionar uma dependência aos Recursos de saúde, é preciso adicionar o repositório Maven do Google ao seu projeto. Para mais informações, consulte a seção Repositório Maven do Google.

Adicione a dependência abaixo ao arquivo build.gradle do módulo:

Groovy

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

Kotlin

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

Verificar os recursos

Antes de se registrar para atualizações de dados, verifique se o dispositivo pode fornecer o tipo de dados de que o app precisa. Ao verificar os recursos, você pode ativar ou desativar algumas funcionalidades ou mudar a interface do app para compensar aqueles que não estão disponíveis.

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
}

Registrar para dados passivos

É possível receber dados passivos usando um serviço ou um callback (ou ambos). Com um serviço, o app pode receber dados em segundo plano quando nenhuma parte dele estiver visível em primeiro plano. Os dados recebidos em segundo plano são enviados em lotes. O callback recebe dados a uma taxa um pouco mais rápida, mas apenas enquanto o app está em execução e o callback é notificado.

Seja qual for o método usado, primeiro crie uma PassiveListenerConfig que determine os tipos de dados que serão recebidos, conforme mostrado no exemplo abaixo:

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

Para receber dados usando um callback, defina e registre esse callback, como mostrado no exemplo abaixo:

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()

O uso de um serviço é semelhante, mas, em vez de criar uma classe derivada de PassiveListenerCallback, derive de PassiveListenerService, conforme mostrado no exemplo abaixo:

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

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

Em seguida, declare o serviço no arquivo AndroidManifest.xml. Exija uma permissão de Recursos de saúde, que garante que somente esses recursos possam se vincular ao serviço:

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

Interpretar o tempo

Os dados que você recebe dos Recursos de saúde são agrupados. Assim, você pode receber pontos de dados de tipos diferentes ou vários pontos de dados do mesmo tipo, no mesmo lote. Use os carimbos de data/hora incluídos nesses objetos em vez da hora em que foram recebidos pelo app para determinar a ordem correta dos eventos.

Para definir o carimbo de data/hora de cada DataPoint, calcule primeiro a hora da inicialização, conforme mostrado no exemplo abaixo:

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

Esse valor pode ser transmitido para getStartInstant() ou getEndInstant().

Restaurar registros após a inicialização

Registros de dados passivos não são mantidos após as reinicializações. Para receber dados após a reinicialização de um dispositivo, recrie seus registros usando um BroadcastReceiver que ouve a transmissão do sistema ACTION_BOOT_COMPLETED.

Não tente restaurar os registros no receptor. Em vez disso, delegue essa função a um worker WorkManager. Quando o dispositivo é inicializado, os Recursos de saúde podem levar dez segundos ou mais para confirmar uma solicitação de registro de dados passivos, o que pode exceder o tempo de execução permitido de um BroadcastReceiver. Por outro lado, os workers WorkManager têm um limite de execução de dez minutos.

O snippet abaixo mostra como um BroadcastReceiver pode ficar:

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()
   }
}

Para que o sistema execute esse código quando o dispositivo for inicializado, faça duas mudanças no AndroidManifest.xml.

Primeiro, adicione a permissão abaixo como filha do <manifest>:

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

Depois, adicione o filtro de intent de receptor abaixo como filho do <application>:

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

Estado da atividade

O cliente passivo também pode fornecer informações de alto nível sobre o estado do usuário, como se o usuário está dormindo. Para receber essas atualizações, siga estas etapas:

  1. Solicite a permissão ACTIVITY_RECOGNITION.
  2. Chame setShouldUserActivityInfoBeRequested(true) no builder PassiveListenerConfig.

Substitua o método onUserActivityInfoReceived() no callback ou serviço e use a UserActivityInfo retornada, conforme mostrado no exemplo abaixo:

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) {
        // ...
    }
}

Metas passivas

É possível configurar um cliente passivo para notificar o app quando as metas passivas forem alcançadas, por exemplo, o usuário concluindo 10.000 passos em um dia.

Para fazer isso, crie uma meta, conforme mostrado no exemplo abaixo:

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)
}

Adicione essa meta à PassiveListenerConfig, conforme mostrado no exemplo abaixo:

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

Substitua o método onGoalCompleted() no callback ou serviço e use o elemento PassiveGoal retornado, conforme mostrado no exemplo abaixo:

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