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

Używaj Menedżera czujników do wypełniania danych o krokach w aplikacji mobilnej w sposób opisany w tym przewodniku. Więcej informacji o projektowaniu i zarządzaniu interfejsem aplikacji do ćwiczeń znajdziesz w artykule Tworzenie podstawowej aplikacji do fitnessu.

Wprowadzenie

Aby zacząć mierzyć liczbę kroków podstawowego licznika kroków na urządzeniu mobilnym, musisz dodać zależności do pliku build.gradle modułu aplikacji. Używaj najnowszych wersji zależności. Jeśli rozszerzysz obsługę aplikacji na inne formaty, takie jak Wear OS, dodaj zależności, których wymagają te formaty.

Poniżej znajdziesz kilka przykładów niektórych 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")

Uzyskaj czujnik 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. Uzyskaj obiekt SensorManager z serwera getSystemService().
  2. Znajdź 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 są wyposażone w czujnik licznika kroków. Sprawdź czujnik i wyświetl komunikat o błędzie, jeśli urządzenie go nie ma:

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

Tworzenie usługi na pierwszym planie

W podstawowej aplikacji do fitnessu możesz mieć przycisk do otrzymywania zdarzeń rozpoczęcia i zakończenia śledzenia kroków użytkownika.

Skorzystaj ze sprawdzonych metod dotyczących czujników. W szczególności czujnik licznika kroków powinien zliczać kroki tylko wtedy, gdy jest zarejestrowany. Powiązanie rejestracji czujnika z usługą działającą na pierwszym planie powoduje, że czujnik rejestruje się tak długo, jak jest to potrzebne, a czujnik pozostaje zarejestrowany, gdy aplikacja nie działa na pierwszym planie.

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

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

Analizowanie danych zdarzeń

Aby uzyskać dostęp do danych z czujnika, zaimplementuj interfejs SensorEventListener. Pamiętaj, że rejestrację czujników należy powiązać z cyklem życia usługi na pierwszym planie, co spowoduje wyrejestrowanie czujnika po jej wstrzymaniu lub zakończeniu. Ten fragment kodu pokazuje, jak wdrożyć interfejs SensorEventListener w 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 na potrzeby zdarzeń związanych z czujnikami

Aplikacja może wyświetlać ekran, na którym użytkownik może sprawdzać swoje kroki w czasie. Aby udostępnić tę funkcję w swojej aplikacji, użyj biblioteki trwałości sal.

Ten fragment kodu tworzy tabelę zawierającą zestaw pomiarów liczby kroków wraz z czasem uzyskania dostępu przez aplikację do poszczególnych pomiarów:

@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ę DAO, utwórz obiekt RoomDatabase:

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

Zapisywanie danych z czujnika w bazie danych

ViewModel używa 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 wyglądałaby 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 czujnika

Jeśli korzystasz z usługi na pierwszym planie, nie musisz konfigurować funkcji WorkManager, ponieważ w okresie, gdy aplikacja aktywnie śledzi kroki użytkownika, powinna pojawić się w niej zaktualizowana łączna liczba kroków.

Jeśli jednak chcesz zgrupować rekordy kroków, możesz użyć funkcji WorkManager, aby mierzyć kroki w określonych odstępach czasu, np. co 15 minut. WorkManager to komponent, który wykonuje w tle pracę na potrzeby gwarantowanego wykonania. Więcej informacji znajdziesz w ćwiczeniach z programowania w usłudze WorkManager.

Aby skonfigurować obiekt Worker do pobierania danych, zastąp metodę doWork() jak w tym 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ć w WorkManager zapisywanie bieżącej liczby kroków co 15 minut, wykonaj te czynności:

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

Ten proces pojawia się 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 bezpośrednio po uruchomieniu aplikacji zainicjować dostawcę treści, który kontroluje dostęp do bazy danych licznika kroków, dodaj do pliku manifestu aplikacji ten element:

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