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

Use o Sensor Manager para preencher dados de passos em um app para dispositivos móveis, conforme descrito neste guia. Para mais informações sobre como criar e gerenciar uma interface de app de exercícios, consulte Criar um app de fitness básico.

Primeiros passos

Para começar a medir as etapas do contador de passos básico no seu dispositivo móvel, adicione as dependências ao arquivo build.gradle do módulo do app. Verifique se você está usando as versões mais recentes das dependências. Além disso, ao estender o suporte do app para outros formatos, como o Wear OS, adicione as dependências necessárias para esses formatos.

Confira alguns exemplos de 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")

Extrair o sensor de contador de passos

Depois que o usuário conceder a permissão de reconhecimento de atividade necessária, você poderá acessar o sensor de contador de passos:

  1. Extraia o objeto SensorManager de getSystemService().
  2. Adquira o sensor de 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 o sensor de contador de passos. Verifique se há um sensor e mostre uma mensagem de erro se o dispositivo não tiver um:

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

Criar o 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 e rastrear etapas.

Siga as práticas recomendadas para sensores. Em particular, o sensor de contador de passos só deve contar passos enquanto o listener do sensor estiver registrado. Ao associar o registro do sensor a um serviço em primeiro plano, o sensor fica registrado enquanto for necessário e pode permanecer registrado quando o app não estiver em primeiro plano.

Use o snippet a seguir para cancelar o registro do sensor no método onPause() do 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. Associe o registro do sensor ao ciclo de vida do serviço em primeiro plano, cancelando o registro quando o serviço for pausado ou encerrado. 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

O app pode mostrar uma tela em que o usuário pode conferir as etapas ao longo do tempo. Para fornecer esse recurso no seu app, use a biblioteca de persistência Room.

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

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

Crie um objeto de acesso a dados (DAO) 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. Assim, você pode armazenar as etapas assim que as 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 dados do sensor periodicamente

Se você usar um serviço em primeiro plano, não será necessário configurar WorkManager porque, durante o período em que o app estiver rastreando ativamente as etapas do usuário, a contagem total de etapas atualizada vai aparecer no app.

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

Para configurar o objeto Worker e recuperar os dados, substitua o método 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 de passos atual a cada 15 minutos, faça o seguinte:

  1. Estenda a classe Application para implementar a interface Configuration.Provider.
  2. No método onCreate(), coloque uma PeriodicWorkRequestBuilder na fila.

Esse processo aparece no seguinte snippet de código:

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

Para inicializar o provedor de conteúdo que controla o acesso ao banco de dados do contador de passos do app imediatamente após a inicialização, adicione o seguinte elemento ao arquivo de manifesto do app:

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