Utiliser le Gestionnaire de capteurs pour mesurer les pas à partir d'un appareil mobile

Utilisez le Sensor Manager pour remplir les données de pas dans une application mobile, comme décrit dans ce guide. Pour en savoir plus sur la conception et la gestion de l'UI d'une application d'exercice, consultez Créer une application de fitness de base.

Premiers pas

Pour commencer à mesurer les pas de votre compteur de pas de base depuis votre appareil mobile, vous devez ajouter les dépendances au fichier build.gradle de votre module d'application. Vérifiez que vous utilisez les dernières versions des dépendances. De plus, lorsque vous étendez la compatibilité de votre application à d'autres facteurs de forme, tels que Wear OS, ajoutez les dépendances requises par ces facteurs de forme.

Voici quelques exemples de dépendances de l'UI. Pour obtenir la liste complète, consultez le guide Éléments d'interface utilisateur.

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

Obtenir le capteur de pas

Une fois que l'utilisateur a accordé l'autorisation de reconnaissance de l'activité nécessaire, vous pouvez accéder au capteur de compteur de pas :

  1. Obtenez l'objet SensorManager à partir de getSystemService().
  2. Obtenez le capteur de compteur de pas à partir 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) }

Certains appareils ne sont pas équipés du capteur de pas. Vous devez rechercher le capteur et afficher un message d'erreur si l'appareil n'en possède pas :

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

Créer votre service de premier plan

Dans une application de fitness de base, vous pouvez avoir un bouton pour recevoir les événements de début et de fin de l'utilisateur afin de suivre les pas.

N'oubliez pas de suivre les bonnes pratiques concernant les capteurs. En particulier, le capteur de compteur de pas ne doit compter les pas que lorsque l'écouteur de capteur est enregistré. En associant l'enregistrement du capteur à un service de premier plan, le capteur est enregistré aussi longtemps que nécessaire et peut rester enregistré lorsque l'application n'est pas au premier plan.

Utilisez l'extrait de code suivant pour désenregistrer le capteur dans la méthode onPause() de votre service de premier plan :

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

Analyser les données pour les événements

Pour accéder aux données du capteur, implémentez l'interface SensorEventListener. Notez que vous devez associer l'enregistrement du capteur au cycle de vie de votre service de premier plan, en désenregistrant le capteur lorsque le service est mis en pause ou arrêté. L'extrait de code suivant montre comment implémenter l'interface SensorEventListener pour 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")
    }
}

Créer une base de données pour les événements de capteur

Votre application peut afficher un écran sur lequel l'utilisateur peut consulter son nombre de pas au fil du temps. Pour offrir cette possibilité à votre application, utilisez la bibliothèque de persistance Room.

L'extrait suivant crée une table contenant un ensemble de mesures du nombre de pas, ainsi que l'heure à laquelle votre application a accédé à chaque mesure :

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

Créez un objet d'accès aux données (DAO) pour lire et écrire les données :

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

Pour instancier le DAO, créez un objet RoomDatabase :

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

Stocker les données du capteur dans la base de données

ViewModel utilise la nouvelle classe StepCounter. Vous pouvez donc stocker les pas dès que vous les lisez :

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

La classe repository se présenterait comme suit :

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


Récupérer périodiquement les données des capteurs

Si vous utilisez un service de premier plan, vous n'avez pas besoin de configurer WorkManager, car le nombre total de pas mis à jour devrait s'afficher dans votre application pendant la période où elle suit activement les pas de l'utilisateur.

Toutefois, si vous souhaitez regrouper vos enregistrements de pas, vous pouvez utiliser WorkManager pour mesurer les pas à un intervalle spécifique, par exemple toutes les 15 minutes. WorkManager est le composant qui effectue le travail en arrière-plan pour une exécution fiable. Pour en savoir plus, consultez l'atelier de programmation WorkManager.

Pour configurer l'objet Worker afin de récupérer les données, remplacez la méthode doWork(), comme indiqué dans l'extrait de code suivant :

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

Pour configurer WorkManager afin qu'il stocke le nombre de pas actuel toutes les 15 minutes, procédez comme suit :

  1. Étendez la classe Application pour implémenter l'interface Configuration.Provider.
  2. Dans la méthode onCreate(), mettez en file d'attente un PeriodicWorkRequestBuilder.

Ce processus est illustré dans l'extrait de code suivant :

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

Pour initialiser le fournisseur de contenu qui contrôle l'accès à la base de données du compteur de pas de votre application immédiatement au démarrage de l'application, ajoutez l'élément suivant au fichier manifeste de votre application :

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