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

Usa Sensor Manager para propagar datos de pasos en una app para dispositivos móviles, como se describe en este . Para obtener más información sobre cómo diseñar y administrar la IU de una app de ejercicios, consultar Compila una app de fitness básica.

Cómo comenzar

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

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, haz lo siguiente: puedes acceder al sensor del contador de pasos:

  1. Obtén el objeto SensorManager de getSystemService().
  2. Adquiere el sensor del contador de pasos de 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 sensor del contador de pasos. Deberías revisar 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")
}

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 realizar un seguimiento de los pasos.

Ten en cuenta las prácticas recomendadas para el sensor. En particular, el sensor del contador de pasos solo debe hacerlo mientras el sensor cuando se registra un objeto de escucha. Asociando el registro del sensor con un primer plano servicio, el sensor se registra durante el tiempo que sea necesario, y este se puede permanecerán registrados 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 para eventos

Para acceder a los datos del sensor, implementa la interfaz SensorEventListener. Nota que debes asociar el registro del sensor con el ID de tu servicio ciclo de vida y cancelar el registro del sensor cuando el servicio esté pausado o finalizado. El El siguiente fragmento 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 podría 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.

Con el siguiente fragmento, se crea una tabla que contiene un conjunto de 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,
)

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

Almacenar los datos del sensor en la base de datos

ViewModel usa la nueva clase StepCounter para que puedas almacenar los pasos en cuanto a medida que 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 los datos de sensores

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

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

Para configurar el objeto Worker a fin de recuperar los datos, anula 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 quieres configurar WorkManager para que almacene el recuento actual de pasos cada 15 minutos, sigue estos pasos: lo siguiente:

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

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 al paso de tu app, sigue estos pasos: contador de datos inmediatamente después de iniciar la app, agrega el siguiente elemento a el archivo de manifiesto de tu app:

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