Usare Gestione sensori per misurare i passi da un dispositivo mobile

Utilizza Sensor Manager per compilare i dati sui passaggi in un'app mobile, come descritto in questa guida. Per scoprire di più su come progettare e gestire la UI di un'app per l'allenamento, consulta l'articolo Creare un'app per il fitness di base.

Per iniziare

Per iniziare a misurare i passi del contapassi di base dal dispositivo mobile, devi aggiungere le dipendenze al file build.gradle del modulo dell'app. Assicurati di utilizzare le versioni più recenti delle dipendenze. Inoltre, quando estendi il supporto della tua app ad altri fattori di forma, come Wear OS, aggiungi le dipendenze richieste da questi fattori.

Di seguito sono riportati alcuni esempi di alcune dipendenze UI. Per un elenco completo, consulta questa guida sugli elementi UI.

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

Ottenere il sensore del contapassi

Dopo che l'utente ha concesso la necessaria autorizzazione di riconoscimento attività, puoi accedere al sensore del contapassi:

  1. Ottieni l'oggetto SensorManager da getSystemService().
  2. Acquisisci il sensore contapassi da SensorManager:
private val sensorManager by lazy {
        getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
        sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }

Alcuni dispositivi non sono dotati del sensore contapassi. Controlla il sensore e mostra un messaggio di errore se il dispositivo non ne ha uno:

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

Crea il servizio in primo piano

In un'app per l'attività fisica di base, potresti avere un pulsante per ricevere gli eventi di inizio e di fine dell'utente per il monitoraggio dei passi.

Tieni presente le best practice relative ai sensori. In particolare, il sensore del contapassi deve contare i passi solo mentre l'ascoltatore del sensore è registrato. Se associ la registrazione del sensore a un servizio in primo piano, il sensore viene registrato per tutto il tempo necessario e può rimanere registrato quando l'app non è in primo piano.

Utilizza il seguente snippet per annullare la registrazione del sensore nel metodo onPause() del servizio in primo piano:

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

Analizzare i dati degli eventi

Per accedere ai dati del sensore, implementa l'interfaccia SensorEventListener. Tieni presente che devi associare la registrazione del sensore al ciclo di vita del servizio in primo piano, annullando la registrazione del sensore quando il servizio è in pausa o terminato. Il seguente snippet mostra come implementare l'interfaccia SensorEventListener per 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 un database per gli eventi dei sensori

L'app potrebbe mostrare una schermata in cui l'utente può vedere i propri passi nel tempo. Per fornire questa funzionalità nella tua app, utilizza la libreria della persistenza della stanza.

Lo snippet seguente crea una tabella contenente un insieme di misurazioni del numero di passi, insieme all'ora in cui la tua app ha eseguito l'accesso a ciascuna misurazione:

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

Crea un oggetto di accesso ai dati (DAO) per leggere e scrivere i dati:

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

Per creare un'istanza del DAO, crea un oggetto RoomDatabase:

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

Archiviare i dati del sensore nel database

ViewModel utilizza la nuova classe StepCounter, pertanto puoi memorizzare i passaggi non appena li leggi:

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

Il corso repository avrà il seguente aspetto:

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


Recuperare periodicamente i dati dei sensori

Se utilizzi un servizio in primo piano, non è necessario configurare WorkManager perché, quando l'app monitora attivamente i passi dell'utente, il conteggio dei passi totale aggiornato dovrebbe essere visualizzato nell'app.

Tuttavia, se vuoi raggruppare i record dei passi, puoi utilizzare WorkManager per misurare i passaggi a intervalli specifici, ad esempio una volta ogni 15 minuti. WorkManager è il componente che esegue le operazioni in background per garantire l'esecuzione. Scopri di più nel codelab di WorkManager.

Per configurare l'oggetto Worker al fine di recuperare i dati, sostituisci il metodo doWork(), come mostrato nel seguente snippet di codice:

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

Per configurare WorkManager in modo che archivi il conteggio dei passi corrente ogni 15 minuti:

  1. Estendi la classe Application per implementare l'interfaccia Configuration.Provider.
  2. Nel metodo onCreate(), accoda un PeriodicWorkRequestBuilder.

Questa procedura viene visualizzata nel seguente snippet di codice:

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

Per inizializzare il fornitore di contenuti che controlla l'accesso al database del contatore di passaggi della tua app subito all'avvio, aggiungi il seguente elemento al file manifest dell'app:

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