Usare Gestione sensori per misurare i passi da un dispositivo mobile

Utilizza Sensor Manager per compilare i dati dei passi in un'app mobile, come descritto in questa guida. Per ulteriori informazioni su come progettare e gestire un'interfaccia utente di un'app per l'allenamento, consulta Creare un'app per il fitness di base.

Per iniziare

Per iniziare a misurare i passi del tuo contapassi di base dal tuo dispositivo mobile, devi aggiungere le dipendenze al file build.gradle del modulo dell'app. Verifica 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 forma.

Di seguito sono riportati alcuni esempi di dipendenze dell'interfaccia utente. Per un elenco completo, consulta questa guida agli elementi dell'interfaccia utente.

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 contapassi

Dopo che l'utente ha concesso l'autorizzazione di riconoscimento dell'attività necessaria, 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 hanno il sensore contapassi. Devi controllare il sensore e mostrare 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 tuo servizio in primo piano

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

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

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

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

Analizzare i dati degli eventi

Per accedere ai dati dei sensori, 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 viene messo 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 del sensore

La tua app potrebbe mostrare una schermata in cui l'utente può visualizzare i suoi passi nel tempo. Per fornire questa funzionalità nella tua app, utilizza la libreria di persistenza Room.

Il seguente snippet crea una tabella che contiene un insieme di misurazioni del conteggio dei passi, insieme all'ora in cui la tua app ha eseguito l'accesso a ogni 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
}

Memorizza i dati dei sensori nel database

ViewModel utilizza la nuova classe StepCounter, in modo da poter memorizzare i passi non appena li leggi:

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

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


Recupero periodico dei dati dei sensori

Se utilizzi un servizio in primo piano, non devi configurare WorkManager perché, durante il periodo in cui la tua app monitora attivamente i passi dell'utente, il conteggio totale aggiornato dei passi dovrebbe essere visualizzato nella tua app.

Se vuoi raggruppare i record dei passi, puoi utilizzare WorkManager per misurare i passi a un intervallo specifico, ad esempio una volta ogni 15 minuti. WorkManager è il componente che esegue il lavoro in background per un'esecuzione affidabile. Scopri di più nel codelab di WorkManager.

Per configurare l'oggetto Worker per recuperare i dati, esegui l'override del 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 memorizzi il conteggio dei passi corrente ogni 15 minuti, procedi come segue:

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

Questa procedura è illustrata 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 content provider che controlla l'accesso al database del contapassi dell'app immediatamente all'avvio dell'app, aggiungi il seguente elemento al file manifest dell'app:

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