센서 관리자를 사용하여 휴대기기에서 걸음 수 측정

센서 관리자를 사용하여 모바일 앱에 걸음 수 데이터를 채웁니다(이 설명 참조). 참조하세요. 운동 앱 UI를 디자인하고 관리하는 방법에 관한 자세한 내용은 참조 기본 피트니스 앱을 빌드합니다.

시작하기

먼저 기본 걸음수 측정기의 걸음 수를 종속 항목을 앱 모듈에 추가해야 합니다. build.gradle 파일. 최신 버전의 종속 항목을 사용해야 합니다. 또한 앱의 지원을 Wear OS와 같은 다른 폼 팩터로 확장하면 이러한 폼 팩터에 필요한 종속 항목을 추가합니다.

다음은 UI 종속 항목의 몇 가지 예입니다. 전체 목록을 보려면 이 UI 요소 가이드를 참조하세요.

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

보행 계수기 센서 가져오기

사용자가 필요한 활동 인식 권한을 부여하면 보행 계수기 센서에 액세스할 수 있습니다.

  1. getSystemService()에서 SensorManager 객체를 가져옵니다.
  2. 다음과 같이 SensorManager에서 보행 계수기를 가져옵니다.
private val sensorManager by lazy {
        getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
        sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }

일부 기기에는 보행 계수기 센서가 없습니다. 센서를 확인해야 합니다 기기에 오류 메시지가 없으면 다음과 같이 오류 메시지를 표시합니다.

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

포그라운드 서비스 만들기

기본 피트니스 앱에는 버튼 를 호출하여 걸음 수 추적을 위한 시작 및 중지 이벤트를 사용자로부터 받아야 합니다.

센서 권장사항을 기억하세요. 특히 보행 계수기 센서는 걸음 수만 계산해야 하는 반면 리스너가 등록됩니다. 센서 등록을 포그라운드와 연결함 센서는 필요한 기간 동안 등록되며 앱이 포그라운드에 있지 않을 때도 등록된 상태로 유지됩니다.

다음 스니펫을 사용하여 onPause() 메서드에서 센서를 등록 취소합니다. 포그라운드 서비스가 포함됩니다.

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

이벤트 데이터 분석

센서 데이터에 액세스하려면 SensorEventListener 인터페이스를 구현합니다. 참고 센서 등록을 포그라운드 서비스의 수명 주기와 같이 서비스가 일시중지되거나 종료될 때 센서를 등록 취소합니다. 이 다음 스니펫은 SensorEventListener 인터페이스를 구현하는 방법을 보여줍니다. 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")
    }
}

센서 이벤트용 데이터베이스 만들기

앱에 사용자가 시간 경과에 따른 걸음 수를 확인할 수 있는 화면이 표시될 수 있습니다. 앱에서 이 기능을 제공하려면 Room 지속성 라이브러리를 사용하세요.

다음 스니펫은 걸음 수 집합이 포함된 테이블을 만듭니다. 앱이 각 측정값에 액세스한 시간과 함께 다음과 같은 데이터가 표시됩니다.

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

데이터 액세스 객체 (DAO) 만들기 데이터를 읽고 씁니다.

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

DAO를 인스턴스화하려면 RoomDatabase 객체를 만듭니다.

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

센서 데이터를 데이터베이스에 저장

ViewModel은 새로운 StepCounter 클래스를 사용하므로 단계를 최대한 빨리 저장할 수 있습니다. 다음과 같습니다.

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

repository 클래스는 다음과 같습니다.

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

주기적으로 센서 데이터 가져오기

포그라운드 서비스를 사용하는 경우 WorkManager를 구성할 필요가 없습니다. 앱이 사용자의 걸음 수를 적극적으로 추적하는 동안 업데이트된 총 걸음 수가 앱에 표시됩니다.

그러나 걸음 수 레코드를 일괄 처리하려면 WorkManager를 사용하여 특정 간격으로 걸음 수를 측정할 수 있습니다(예: 15분마다 한 번). WorkManager는 백그라운드를 실행하는 구성요소입니다. 보장된 실행을 위해 작동합니다 WorkManager Codelab에서 자세히 알아보세요.

데이터를 가져오도록 Worker 객체를 구성하려면 doWork()를 재정의합니다. 메서드를 사용하세요.

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

15분마다 현재 걸음 수를 저장하도록 WorkManager를 설정하려면 다음 단계를 따르세요. 다음과 같습니다.

  1. Application 클래스를 확장하여 Configuration.Provider 구현 인터페이스에 추가되었습니다.
  2. onCreate() 메서드에서 PeriodicWorkRequestBuilder를 큐에 추가합니다.

이 프로세스는 다음 코드 스니펫에 나와 있습니다.

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

앱 단계에 대한 액세스를 제어하는 콘텐츠 제공자 초기화 카운터 데이터베이스 대신 다음 요소를 추가하여 앱의 매니페스트 파일에 있습니다.

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