Mit dem Sensor Manager Schritte von einem Mobilgerät aus messen

Mit dem Sensor Manager können Sie Schrittdaten in einer mobilen App erfassen, wie in diesem Leitfaden beschrieben. Weitere Informationen zum Entwerfen und Verwalten der Benutzeroberfläche einer Trainings-App findest du unter Einfache Fitness-App erstellen.

Erste Schritte

Wenn Sie die Schritte Ihres einfachen Schrittzählers auf Ihrem Mobilgerät messen möchten, müssen Sie die Abhängigkeiten der build.gradle-Datei Ihres App-Moduls hinzufügen. Prüfen Sie, ob Sie die neuesten Versionen der Abhängigkeiten verwenden. Wenn Sie die Unterstützung Ihrer App auf andere Formfaktoren wie Wear OS ausweiten, fügen Sie die für diese Formfaktoren erforderlichen Abhängigkeiten hinzu.

Im Folgenden finden Sie einige Beispiele für UI-Abhängigkeiten. Eine vollständige Liste finden Sie in diesem Leitfaden zu UI-Elementen.

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

Schrittzähler-Sensor abrufen

Nachdem der Nutzer die erforderliche Berechtigung zur Aktivitätserkennung erteilt hat, können Sie auf den Schrittzähler zugreifen:

  1. Rufen Sie das Objekt SensorManager aus getSystemService() ab.
  2. Rufen Sie den Schrittzähler-Sensor über SensorManager ab:
private val sensorManager by lazy {
        getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
        sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }

Einige Geräte haben keinen Schrittzähler-Sensor. Sie sollten nach dem Sensor suchen und eine Fehlermeldung anzeigen, wenn das Gerät keinen hat:

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

Vordergrunddienst erstellen

In einer einfachen Fitness-App haben Sie möglicherweise eine Schaltfläche, über die Start- und Stopp-Ereignisse vom Nutzer empfangen werden, um Schritte zu erfassen.

Beachten Sie die Best Practices für Sensoren. Insbesondere sollte der Schrittzähler nur Schritte zählen, wenn der Sensor-Listener registriert ist. Durch die Verknüpfung der Sensorregistrierung mit einem Vordergrunddienst wird der Sensor so lange registriert, wie er benötigt wird. Er kann auch dann registriert bleiben, wenn die App nicht im Vordergrund ausgeführt wird.

Verwenden Sie das folgende Snippet, um den Sensor in der Methode onPause() Ihres Vordergrunddiensts zu deregistrieren:

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

Daten für Ereignisse analysieren

Implementieren Sie die SensorEventListener-Schnittstelle, um auf die Sensordaten zuzugreifen. Die Sensorregistrierung sollte mit dem Lebenszyklus Ihres Vordergrunddienstes verknüpft werden. Heben Sie die Registrierung des Sensors auf, wenn der Dienst pausiert oder beendet wird. Das folgende Snippet zeigt, wie die SensorEventListener-Schnittstelle für Sensor.TYPE_STEP_COUNTER implementiert wird:

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

Datenbank für die Sensorereignisse erstellen

Ihre App zeigt möglicherweise einen Bildschirm an, auf dem der Nutzer seine Schritte im Zeitverlauf sehen kann. Wenn Sie diese Funktion in Ihrer App anbieten möchten, verwenden Sie die Room-Persistenzbibliothek.

Im folgenden Snippet wird eine Tabelle mit einer Reihe von Schrittzählermessungen erstellt, zusammen mit der Uhrzeit, zu der Ihre App auf die einzelnen Messungen zugegriffen hat:

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

Erstellen Sie ein DAO (Data Access Object), um die Daten zu lesen und zu schreiben:

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

Erstellen Sie zum Instanziieren des DAO ein RoomDatabase-Objekt:

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

Sensordaten in der Datenbank speichern

Das ViewModel verwendet die neue StepCounter-Klasse, sodass du die Schritte sofort nach dem Lesen speichern kannst:

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

Die repository-Klasse würde so aussehen:

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


Regelmäßiges Abrufen von Sensordaten

Wenn Sie einen Dienst im Vordergrund verwenden, müssen Sie WorkManager nicht konfigurieren, da die aktualisierte Gesamtzahl der Schritte in Ihrer App angezeigt werden sollte, während Ihre App die Schritte des Nutzers aktiv erfasst.

Wenn Sie Ihre Schrittaufzeichnungen in Batches erfassen möchten, können Sie WorkManager verwenden, um Schritte in einem bestimmten Intervall zu messen, z. B. alle 15 Minuten. WorkManager ist die Komponente, die die Hintergrundarbeit für eine zuverlässige Ausführung übernimmt. Weitere Informationen finden Sie im WorkManager-Codelab.

Wenn Sie das Worker-Objekt zum Abrufen der Daten konfigurieren möchten, überschreiben Sie die Methode doWork(), wie im folgenden Code-Snippet gezeigt:

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

So richtest du WorkManager ein, damit die aktuelle Schrittzahl alle 15 Minuten gespeichert wird:

  1. Erweitern Sie die Klasse Application, um die Schnittstelle Configuration.Provider zu implementieren.
  2. Stellen Sie in der Methode onCreate() eine PeriodicWorkRequestBuilder in die Warteschlange.

Dieser Prozess wird im folgenden Code-Snippet dargestellt:

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

Wenn Sie den Content-Provider, der den Zugriff auf die Schrittzählerdatenbank Ihrer App steuert, sofort nach dem Start der App initialisieren möchten, fügen Sie der Manifestdatei Ihrer App das folgende Element hinzu:

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