Używaj Menedżera czujników do mierzenia kroków na urządzeniu mobilnym

Użyj Menedżera czujników, aby wypełnić dane o krokach w aplikacji mobilnej, zgodnie z opisem w tym przewodniku. Więcej informacji o projektowaniu interfejsu aplikacji do ćwiczeń i zarządzaniu nim znajdziesz w artykule Tworzenie podstawowej aplikacji do ćwiczeń.

Pierwsze kroki

Aby rozpocząć pomiar kroków za pomocą podstawowego krokomierza na urządzeniu mobilnym, musisz dodać zależności do pliku modułu aplikacji build.gradle. Sprawdź, czy używasz najnowszych wersji zależności. Jeśli rozszerzysz obsługę aplikacji na inne formaty, takie jak Wear OS, dodaj zależności wymagane przez te formaty.

Oto kilka przykładów zależności interfejsu: Pełną listę znajdziesz w tym przewodniku po elementach interfejsu.

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

Uzyskiwanie dostępu do czujnika licznika kroków

Gdy użytkownik przyzna niezbędne uprawnienia do rozpoznawania aktywności, możesz uzyskać dostęp do czujnika licznika kroków:

  1. Pobierz obiekt SensorManagergetSystemService().
  2. Pobierz czujnik licznika kroków z SensorManager:
private val sensorManager by lazy {
        getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
        sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }

Niektóre urządzenia nie mają czujnika licznika kroków. Sprawdź, czy urządzenie ma czujnik, i wyświetl komunikat o błędzie, jeśli go nie ma:

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

Tworzenie usługi działającej na pierwszym planie

W podstawowej aplikacji do fitnessu możesz mieć przycisk, który będzie odbierać od użytkownika zdarzenia rozpoczęcia i zakończenia śledzenia kroków.

Pamiętaj o sprawdzonych metodach dotyczących czujników. W szczególności czujnik licznika kroków powinien zliczać kroki tylko wtedy, gdy zarejestrowany jest odbiornik czujnika. Dzięki powiązaniu rejestracji czujnika z usługą na pierwszym planie czujnik jest zarejestrowany tak długo, jak jest to potrzebne, i może pozostać zarejestrowany, gdy aplikacja nie jest na pierwszym planie.

Aby wyrejestrować czujnik w metodzie onPause() usługi na pierwszym planie, użyj tego fragmentu kodu:

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

Analizowanie danych dotyczących zdarzeń

Aby uzyskać dostęp do danych z czujników, zaimplementuj interfejs SensorEventListener. Pamiętaj, że rejestrację czujnika należy powiązać z cyklem życia usługi działającej na pierwszym planie, a wyrejestrować czujnik, gdy usługa zostanie wstrzymana lub zakończona. Poniższy fragment kodu pokazuje, jak wdrożyć interfejs SensorEventListener dla 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")
    }
}

Tworzenie bazy danych zdarzeń czujnika

Aplikacja może wyświetlać ekran, na którym użytkownik może zobaczyć liczbę kroków w danym okresie. Aby udostępnić tę funkcję w aplikacji, użyj biblioteki trwałości danych Room.

Poniższy fragment kodu tworzy tabelę zawierającą zestaw pomiarów liczby kroków wraz z czasem, w którym aplikacja uzyskała dostęp do każdego pomiaru:

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

Utwórz obiekt dostępu do danych (DAO), aby odczytywać i zapisywać dane:

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

Aby utworzyć instancję obiektu DAO, utwórz obiekt RoomDatabase:

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

Przechowywanie danych z czujników w bazie danych

ViewModel korzysta z nowej klasy StepCounter, dzięki czemu możesz zapisywać kroki od razu po ich odczytaniu:

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

Klasa repository będzie wyglądać tak:

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


Okresowe pobieranie danych z czujników

Jeśli używasz usługi działającej na pierwszym planie, nie musisz konfigurować WorkManager, ponieważ w czasie, gdy aplikacja aktywnie śledzi kroki użytkownika, zaktualizowana łączna liczba kroków powinna być widoczna w aplikacji.

Jeśli jednak chcesz rejestrować kroki w pakietach, możesz użyć ikony WorkManager, aby mierzyć kroki w określonych odstępach czasu, np. co 15 minut. WorkManager to komponent, który wykonuje pracę w tle, aby zapewnić niezawodne działanie. Więcej informacji znajdziesz w samouczku WorkManager.

Aby skonfigurować obiekt Worker do pobierania danych, zastąp metodę doWork(), jak pokazano w poniższym fragmencie kodu:

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

Aby skonfigurować WorkManager tak, aby co 15 minut zapisywał bieżącą liczbę kroków, wykonaj te czynności:

  1. Rozszerz klasę Application, aby zaimplementować interfejs Configuration.Provider.
  2. W metodzie onCreate() umieść w kolejce PeriodicWorkRequestBuilder.

Ten proces jest widoczny w tym fragmencie kodu:

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

Aby zainicjować dostawcę treści, który kontroluje dostęp do bazy danych licznika kroków aplikacji, natychmiast po uruchomieniu aplikacji, dodaj ten element do pliku manifestu aplikacji:

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