Sử dụng Trình quản lý cảm biến để đo số bước trên thiết bị di động

Sử dụng Trình quản lý cảm biến để điền sẵn dữ liệu về số bước trong một ứng dụng di động, như mô tả trong hướng dẫn này. Để biết thêm thông tin về cách thiết kế và quản lý giao diện người dùng của ứng dụng tập thể dục, hãy tham khảo bài viết Tạo một ứng dụng thể dục cơ bản.

Bắt đầu

Để bắt đầu đo lường số bước của bộ đếm bước cơ bản trên thiết bị di động, bạn cần thêm các phần phụ thuộc vào tệp build.gradle của mô-đun ứng dụng. Xác minh rằng bạn sử dụng các phiên bản mới nhất của các phần phụ thuộc. Ngoài ra, khi bạn mở rộng phạm vi hỗ trợ của ứng dụng sang các kiểu dáng khác, chẳng hạn như Wear OS, hãy thêm các phần phụ thuộc mà những kiểu dáng này yêu cầu.

Sau đây là một vài ví dụ về một số thành phần phụ thuộc của giao diện người dùng. Để xem danh sách đầy đủ, hãy tham khảo hướng dẫn về Phần tử trên giao diện người dùng này.

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

Lấy cảm biến bộ đếm bước

Sau khi người dùng cấp quyền nhận dạng hoạt động cần thiết, bạn có thể truy cập vào cảm biến bộ đếm bước:

  1. Lấy đối tượng SensorManager từ getSystemService().
  2. Thu thập cảm biến bộ đếm bước từ SensorManager:
private val sensorManager by lazy {
        getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
        sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }

Một số thiết bị không có cảm biến bộ đếm bước. Bạn nên kiểm tra cảm biến và hiện thông báo lỗi nếu thiết bị không có cảm biến:

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

Tạo dịch vụ trên nền trước

Trong một ứng dụng thể dục cơ bản, bạn có thể có một nút để nhận các sự kiện bắt đầu và dừng từ người dùng nhằm theo dõi số bước.

Hãy lưu ý các phương pháp hay nhất về cảm biến. Cụ thể, cảm biến bộ đếm bước chỉ nên đếm các bước trong khi trình nghe cảm biến được đăng ký. Bằng cách liên kết quá trình đăng ký cảm biến với một dịch vụ trên nền trước, cảm biến sẽ được đăng ký miễn là cần thiết và cảm biến có thể vẫn được đăng ký khi ứng dụng không ở trên nền trước.

Sử dụng đoạn mã sau để huỷ đăng ký cảm biến trong phương thức onPause() của dịch vụ trên nền trước:

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

Phân tích dữ liệu cho các sự kiện

Để truy cập vào dữ liệu cảm biến, hãy triển khai giao diện SensorEventListener. Xin lưu ý rằng bạn nên liên kết quá trình đăng ký cảm biến với vòng đời của dịch vụ trên nền trước, huỷ đăng ký cảm biến khi dịch vụ bị tạm dừng hoặc kết thúc. Đoạn mã sau đây cho biết cách triển khai giao diện SensorEventListener cho 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")
    }
}

Tạo cơ sở dữ liệu cho các sự kiện cảm biến

Ứng dụng của bạn có thể hiển thị một màn hình nơi người dùng có thể xem số bước của họ theo thời gian. Để cung cấp chức năng này trong ứng dụng, hãy sử dụng thư viện lưu trữ Room.

Đoạn mã sau đây tạo một bảng chứa một nhóm các phép đo số bước, cùng với thời gian mà ứng dụng của bạn truy cập vào từng phép đo:

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

Tạo một đối tượng truy cập dữ liệu (DAO) để đọc và ghi dữ liệu:

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

Để tạo thực thể DAO, hãy tạo một đối tượng RoomDatabase:

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

Lưu trữ dữ liệu cảm biến vào cơ sở dữ liệu

ViewModel sử dụng lớp StepCounter mới, vì vậy, bạn có thể lưu trữ các bước ngay khi đọc chúng:

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

Lớp repository sẽ có dạng như sau:

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


Định kỳ truy xuất dữ liệu cảm biến

Nếu sử dụng dịch vụ trên nền trước, bạn không cần định cấu hình WorkManager vì trong thời gian ứng dụng của bạn đang tích cực theo dõi các bước của người dùng, tổng số bước được cập nhật sẽ xuất hiện trong ứng dụng của bạn.

Tuy nhiên, nếu muốn xử lý hàng loạt các bản ghi bước, bạn có thể sử dụng WorkManager để đo số bước theo một khoảng thời gian cụ thể, chẳng hạn như 15 phút một lần. WorkManager là thành phần thực hiện công việc ở chế độ nền để đảm bảo quá trình thực thi diễn ra đáng tin cậy. Tìm hiểu thêm trong lớp học lập trình WorkManager.

Để định cấu hình đối tượng Worker nhằm truy xuất dữ liệu, hãy ghi đè phương thức doWork(), như trong đoạn mã sau:

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

Để thiết lập WorkManager lưu trữ số bước hiện tại sau mỗi 15 phút, hãy làm như sau:

  1. Mở rộng lớp Application để triển khai giao diện Configuration.Provider.
  2. Trong phương thức onCreate(), hãy xếp hàng đợi một PeriodicWorkRequestBuilder.

Quy trình này xuất hiện trong đoạn mã sau:

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

Để khởi động trình cung cấp nội dung kiểm soát quyền truy cập vào cơ sở dữ liệu bộ đếm bước của ứng dụng ngay khi khởi động ứng dụng, hãy thêm phần tử sau vào tệp kê khai của ứng dụng:

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