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

센서 관리자를 사용하여 이 가이드에 설명된 대로 모바일 앱의 단계 데이터를 채웁니다. 운동 앱 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 인터페이스를 구현합니다. 센서 등록을 포그라운드 서비스의 수명 주기와 연결하여 서비스가 일시중지되거나 종료되면 센서 등록을 취소해야 합니다. 다음 스니펫은 Sensor.TYPE_STEP_COUNTERSensorEventListener 인터페이스를 구현하는 방법을 보여줍니다.

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" />