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 dữ liệu về số bước vào ứ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 ứng dụng thể dục cơ bản.

Bắt đầu

Để bắt đầu đo các 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. Đảm bảo rằng bạn sử dụng phiên bản mới nhất của phần phụ thuộc. Ngoài ra, khi mở rộng khả năng hỗ trợ ứng dụng sang các hệ số hình dạng khác, chẳng hạn như Wear OS, hãy thêm các phần phụ thuộc mà các hệ số hình dạng này yêu cầu.

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

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. Có được 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à kết thúc từ người dùng cho số bước theo dõi.

Hãy lưu ý đến 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 số bước trong khi trình nghe cảm biến được đăng ký. Bằng cách liên kết hoạt động đăng ký cảm biến với dịch vụ trên nền trước, cảm biến sẽ được đăng ký ngay khi cần thiết và cảm biến có thể vẫn được đăng ký khi ứng dụng không chạy ở nền trước.

Hãy 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 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 việc đă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 để người dùng xem số bước của họ theo thời gian. Để cung cấp chức năng này trong ứng dụng của bạn, hãy dùng Thư viện lưu trữ Room.

Đoạn mã sau đây sẽ tạo một bảng chứa một tập hợp các thông tin đo lường số bước, cùng với thời điểm ứng dụng của bạn truy cập vào từng thông tin đo lường:

@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 để bạn có thể lưu trữ các bước ngay khi bạn đọc:

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 chủ động theo dõi số bước của người dùng, tổng số bước đã cập nhật sẽ xuất hiện trong ứng dụng.

Tuy nhiên, nếu muốn phân lô các bản ghi số bước, bạn có thể sử dụng WorkManager để đo lường các bước trong 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 trong nền để thực thi được đảm bảo. 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 nhằm lưu trữ số bước hiện tại 15 phút một lần, 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 thêm một PeriodicWorkRequestBuilder vào hàng đợi.

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 chạy 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" />