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

Usa Sensor Manager para completar los datos de pasos en una app para dispositivos móviles, como se describe en esta guía. Para obtener más información sobre cómo diseñar y administrar la IU de una app de ejercicios, consulta Cómo compilar una app de fitness básica.

Cómo comenzar

Para comenzar a medir los pasos de tu 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. Verifica que uses 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 de forma.

Estos son algunos ejemplos de las dependencias de la IU. Para obtener una lista completa, consulta esta guía de 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 de contador de pasos

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

  1. Obtén el objeto SensorManager de getSystemService().
  2. Adquiere el sensor de contador de pasos del 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 de contador de pasos. Debes verificar si el sensor está presente 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")
}

Crea tu servicio en primer plano

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

Ten en cuenta las prácticas recomendadas para los sensores. En particular, el sensor de contador de pasos solo debe contar los pasos mientras el objeto de escucha del sensor esté registrado. Al asociar el registro del sensor con un servicio en primer plano, el sensor se registra mientras 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)
}

Analiza los datos de los 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 detenga o finalice. 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 del sensor

Es posible que tu app muestre una pantalla en la que el usuario pueda ver sus pasos a lo largo del tiempo. Para proporcionar esta capacidad en tu app, usa la biblioteca de persistencia Room.

El siguiente fragmento crea una tabla que contiene un conjunto de mediciones de recuento de pasos, junto con la hora en la que tu 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 a 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

El ViewModel usa la nueva clase StepCounter, por lo que puedes almacenar los pasos tan pronto como los leas:

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

La clase repository se vería 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
            }
        }
    }
}


Recuperación periódica de datos de sensores

Si usas un servicio en primer plano, no es necesario que configures WorkManager, ya que, durante el tiempo en que tu app realiza un seguimiento activo de los pasos del usuario, el recuento total de pasos actualizado debería aparecer en tu app.

Sin embargo, si quieres agrupar tus registros de pasos, 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 confiable. Obtén más información en el codelab de WorkManager.

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

Para configurar WorkManager de modo 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(), pon en cola un PeriodicWorkRequestBuilder.

Este proceso se muestra 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 del inicio de 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" />