Usar o Gerenciador de sensores para medir passos em um dispositivo móvel

Use o Gerenciador de sensores para preencher dados de passos em um app para dispositivos móveis, conforme descrito guia. Para mais informações sobre como projetar e gerenciar uma interface de app de exercícios, consulte Criar um app básico de condicionamento físico.

Primeiros passos

Para começar a medir as etapas do contador básico de passos do dispositivo móvel, será necessário adicionar as dependências ao módulo do app build.gradle. Use as versões mais recentes das dependências. Além disso, quando você estende a compatibilidade do app a outros formatos, como Wear OS, adicione as dependências exigidas por esses formatos.

Confira abaixo alguns exemplos de algumas das dependências da interface. Para uma lista completa, consulte este guia de Elementos da interface.

implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.activity:activity-compose")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material:material")

Acessar o sensor do contador de passos

Depois que o usuário conceder a permissão de reconhecimento de atividades necessária, acesse o sensor do contador de passos:

  1. Consiga o objeto SensorManager de getSystemService().
  2. Acesse o sensor contador de passos do SensorManager:
private val sensorManager by lazy {
        getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
        sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }

Alguns dispositivos não têm esse sensor. Verifique se o sensor e mostrar uma mensagem de erro se o dispositivo não tiver uma:

if (sensor == null) {
    Text(text = "Step counter sensor is not present on this device")
}

Criar um serviço em primeiro plano

Em um app fitness básico, você pode ter um botão para receber eventos de início e parada do usuário para rastrear as etapas.

Atenção às práticas recomendadas de sensores. Em particular, o sensor contador de passos conta apenas os passos, listener está registrado. Associando o registro do sensor a um primeiro plano serviço, o sensor fica registrado pelo tempo necessário e pode permanecem registrados quando o app não está em primeiro plano.

Use o snippet a seguir para cancelar o registro do sensor no método onPause() de seu serviço em primeiro plano:

override fun onPause() {
    super.onPause()
    sensorManager.unregisterListener(this)
}

Analisar dados de eventos

Para acessar os dados do sensor, implemente a interface SensorEventListener. Observação que você deve associar o registro do sensor aos arquivos cancelar o registro do sensor quando o serviço for pausado ou encerrado. A O snippet a seguir mostra como implementar a interface SensorEventListener. para Sensor.TYPE_STEP_COUNTER:

private const val TAG = "STEP_COUNT_LISTENER"

context(Context)
class StepCounter {
    private val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
    private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)

    suspend fun steps() = suspendCancellableCoroutine { continuation ->
        Log.d(TAG, "Registering sensor listener... ")

        val listener: SensorEventListener by lazy {
            object : SensorEventListener {
                override fun onSensorChanged(event: SensorEvent?) {
                    if (event == null) return

                    val stepsSinceLastReboot = event.values[0].toLong()
                    Log.d(TAG, "Steps since last reboot: $stepsSinceLastReboot")

                    if (continuation.isActive) {
                        continuation.resume(stepsSinceLastReboot)
                    }
                }

                override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
                      Log.d(TAG, "Accuracy changed to: $accuracy")
                }
            }
       }

        val supportedAndEnabled = sensorManager.registerListener(listener,
                sensor, SensorManager.SENSOR_DELAY_UI)
        Log.d(TAG, "Sensor listener registered: $supportedAndEnabled")
    }
}

Criar um banco de dados para os eventos do sensor

Seu app pode mostrar uma tela em que o usuário possa conferir os passos ao longo do tempo. Para oferecer esse recurso no seu app, use a biblioteca de persistência do Room.

O snippet a seguir cria uma tabela que contém um conjunto de contagem de passos medições, além do momento em que seu app acessou cada medição:

@Entity(tableName = "steps")
data class StepCount(
  @ColumnInfo(name = "steps") val steps: Long,
  @ColumnInfo(name = "created_at") val createdAt: String,
)

Criar um objeto de acesso a dados (DAO, na sigla em inglês) para ler e gravar os dados:

@Dao
interface StepsDao {
    @Query("SELECT * FROM steps")
    suspend fun getAll(): List<StepCount>

    @Query("SELECT * FROM steps WHERE created_at >= date(:startDateTime) " +
            "AND created_at < date(:startDateTime, '+1 day')")
    suspend fun loadAllStepsFromToday(startDateTime: String): Array<StepCount>

    @Insert
    suspend fun insertAll(vararg steps: StepCount)

    @Delete
    suspend fun delete(steps: StepCount)
}

Para instanciar o DAO, crie um objeto RoomDatabase:

@Database(entities = [StepCount::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun stepsDao(): StepsDao
}

Armazenar os dados do sensor no banco de dados

O ViewModel usa a nova classe StepCounter para que você possa armazenar as etapas assim que possível. enquanto os lê:

viewModelScope.launch {
    val stepsFromLastBoot = stepCounter.steps()
    repository.storeSteps(stepsFromLastBoot)
}

A classe repository ficaria assim:

class Repository(
    private val stepsDao: StepsDao,
) {

    suspend fun storeSteps(stepsSinceLastReboot: Long) = withContext(Dispatchers.IO) {
        val stepCount = StepCount(
            steps = stepsSinceLastReboot,
            createdAt = Instant.now().toString()
        )
        Log.d(TAG, "Storing steps: $stepCount")
        stepsDao.insertAll(stepCount)
    }

    suspend fun loadTodaySteps(): Long = withContext(Dispatchers.IO) {
        printTheWholeStepsTable() // DEBUG

        val todayAtMidnight = (LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT).toString())
        val todayDataPoints = stepsDao.loadAllStepsFromToday(startDateTime = todayAtMidnight)
        when {
            todayDataPoints.isEmpty() -> 0
            else -> {
                val firstDataPointOfTheDay = todayDataPoints.first()
                val latestDataPointSoFar = todayDataPoints.last()

                val todaySteps = latestDataPointSoFar.steps - firstDataPointOfTheDay.steps
                Log.d(TAG, "Today Steps: $todaySteps")
                todaySteps
            }
        }
    }
}

Recuperar periodicamente os dados do sensor

Se você usa um serviço em primeiro plano, não é necessário configurar o WorkManager porque, durante o tempo em que seu aplicativo monitora ativamente os passos do usuário, a contagem total de passos atualizada será exibida no aplicativo.

No entanto, se você quiser agrupar os registros de passos em lote, use WorkManager para medir passos em um intervalo específico, como uma vez a cada 15 minutos. WorkManager é o componente que executa o segundo plano. para execução garantida. Saiba mais no codelab do WorkManager.

Para configurar o objeto Worker para extrair os dados, substitua doWork() , conforme mostrado no snippet de código a seguir:

private const val TAG = " StepCounterWorker"

@HiltWorker
class StepCounterWorker @AssistedInject constructor(
    @Assisted appContext: Context,
    @Assisted workerParams: WorkerParameters,
    val repository: Repository,
    val stepCounter: StepCounter
) : CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        Log.d(TAG, "Starting worker...")

        val stepsSinceLastReboot = stepCounter.steps().first()
        if (stepsSinceLastReboot == 0L) return Result.success()

        Log.d(TAG, "Received steps from step sensor: $stepsSinceLastReboot")
        repository.storeSteps(stepsSinceLastReboot)

        Log.d(TAG, "Stopping worker...")
        return Result.success()
    }
}

Para configurar o WorkManager para armazenar a contagem atual de passos a cada 15 minutos, faça o seguinte: o seguinte:

  1. Estenda a classe Application para implementar o Configuration.Provider. interface gráfica do usuário.
  2. No método onCreate(), coloque um PeriodicWorkRequestBuilder na fila.

Esse processo aparece no snippet de código a seguir:

@HiltAndroidApp
@RequiresApi(Build.VERSION_CODES.S)
internal class PulseApplication : Application(), Configuration.Provider {

    @Inject
    lateinit var workerFactory: HiltWorkerFactory

    override fun onCreate() {
        super.onCreate()

        val myWork = PeriodicWorkRequestBuilder<StepCounterWorker>(
                15, TimeUnit.MINUTES).build()

        WorkManager.getInstance(this)
            .enqueueUniquePeriodicWork("MyUniqueWorkName",
                    ExistingPeriodicWorkPolicy.UPDATE, myWork)
    }

    override val workManagerConfiguration: Configuration
        get() = Configuration.Builder()
            .setWorkerFactory(workerFactory)
            .setMinimumLoggingLevel(android.util.Log.DEBUG)
            .build()
}

Inicializar o provedor de conteúdo que controla o acesso à etapa do seu app do banco de dados de contadores imediatamente após a inicialização do app, adicione o seguinte elemento ao arquivo de manifesto do seu app:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove" />