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

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

Como começar

Para começar a medir as etapas do seu 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 a outros formatos, como o Wear OS, adicione as dependências exigidas por esses formatos.

Confira abaixo alguns exemplos de algumas das dependências da interface. Para conferir 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")

Encontrar o sensor contador de passos

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

  1. Extraia o objeto SensorManager do getSystemService().
  2. Adquira 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 o 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 monitorar passos.

Confira as práticas recomendadas sobre sensores. Especificamente, o sensor contador só vai contar passos enquanto o listener estiver registrado. Ao associar o registro do sensor a um serviço em primeiro plano, o sensor é registrado pelo tempo que for necessário, podendo permanecer registrado quando o app não está 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 do sensor 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 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 medidas de contagem de passos, além do horário 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,
)

Crie 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 as ler:

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 precisa configurar WorkManager porque, durante o período em que o app está monitorando ativamente os passos do usuário, a contagem total de passos atualizada aparece no app.

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

Para configurar o objeto Worker para extrair 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 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()
}

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" />