Usa el Administrador de sensores para medir los pasos desde un dispositivo móvil

Usa el Administrador de sensores para propagar los datos de los pasos en una app para dispositivos móviles, como se describe en esta guía. Si quieres obtener más información para diseñar y administrar la IU de una app de ejercicios, consulta Cómo compilar una app de fitness básica.

Cómo empezar

Para comenzar a medir los pasos del contador de pasos básico desde tu dispositivo móvil, deberás agregar las dependencias al archivo build.gradle del módulo de tu app. Asegúrate de usar las versiones más recientes de las dependencias. Además, cuando extiendas la compatibilidad de tu app a otros factores de forma, como Wear OS, agrega las dependencias que requieren estos factores.

A continuación, se muestran algunos ejemplos de algunas de las dependencias de la IU. Para obtener una lista completa, consulta esta guía sobre elementos de la IU.

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

Obtén el sensor del contador de pasos

Después de que el usuario otorgue el permiso de reconocimiento de actividad necesario, podrás acceder al sensor del contador de pasos:

  1. Obtén el objeto SensorManager de getSystemService().
  2. Adquiere el sensor del contador de pasos en SensorManager:
private val sensorManager by lazy {
        getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
        sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }

Algunos dispositivos no tienen el sensor del contador de pasos. Debes buscar el sensor y mostrar un mensaje de error si el dispositivo no tiene uno:

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

Cómo crear tu servicio en primer plano

En una app básica de fitness, es posible que tengas un botón para recibir eventos de inicio y detención del usuario y, así, hacer un seguimiento de los pasos.

Ten en cuenta las prácticas recomendadas relacionadas con los sensores. En particular, el sensor del contador de pasos solo debe contar pasos mientras el objeto de escucha del sensor está registrado. Cuando se asocia el registro del sensor con un servicio en primer plano, el sensor se registra siempre que sea necesario y puede permanecer registrado cuando la app no está en primer plano.

Usa el siguiente fragmento para cancelar el registro del sensor en el método onPause() de tu servicio en primer plano:

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

Analizar datos en busca de eventos

Para acceder a los datos del sensor, implementa la interfaz SensorEventListener. Ten en cuenta que debes asociar el registro del sensor con el ciclo de vida de tu servicio en primer plano y cancelar el registro del sensor cuando el servicio se pone en pausa o finaliza. En el siguiente fragmento, se muestra cómo implementar la interfaz 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")
    }
}

Crea una base de datos para los eventos de sensores

Tu app puede mostrar una pantalla en la que el usuario pueda ver sus pasos a lo largo del tiempo. Para proporcionar esta función en tu app, usa la biblioteca de persistencias Room.

En el siguiente fragmento, se crea una tabla que contiene un conjunto de mediciones del recuento de pasos, junto con la hora en la que la app accedió a cada medición:

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

Crea un objeto de acceso de datos (DAO) para leer y escribir los datos:

@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 crear una instancia del DAO, crea un objeto RoomDatabase:

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

Almacena los datos del sensor en la base de datos

ViewModel usa la nueva clase StepCounter, por lo que puedes almacenar los pasos en cuanto los leas:

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

La clase repository se verá de la siguiente manera:

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


Cómo recuperar datos del sensor de forma periódica

Si usas un servicio en primer plano, no necesitas configurar WorkManager porque, durante el tiempo en que la app realiza un seguimiento activo de los pasos del usuario, el recuento total actualizado debería aparecer en la app.

Sin embargo, si deseas agrupar los registros de pasos por lotes, puedes usar WorkManager para medir los pasos en un intervalo específico, por ejemplo, una vez cada 15 minutos. WorkManager es el componente que realiza el trabajo en segundo plano para una ejecución garantizada. Obtén más información en el codelab de WorkManager.

Si deseas configurar el objeto Worker para recuperar los datos, anula el método doWork(), como se muestra en el siguiente fragmento de código:

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

Si deseas configurar WorkManager para que almacene el recuento de pasos actual cada 15 minutos, haz lo siguiente:

  1. Extiende la clase Application para implementar la interfaz Configuration.Provider.
  2. En el método onCreate(), coloca un PeriodicWorkRequestBuilder en la cola.

Este proceso aparece en el siguiente fragmento 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 el proveedor de contenido que controla el acceso a la base de datos del contador de pasos de tu app inmediatamente después de que se inicia la app, agrega el siguiente elemento al archivo de manifiesto de la app:

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