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

이 가이드에 설명된 대로 센서 관리자를 사용하여 모바일 앱에 걸음 수 데이터를 채웁니다. 운동 앱 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()
    }
}

WorkManager이 15분마다 현재 걸음 수를 저장하도록 설정하려면 다음을 실행하세요.

  1. Configuration.Provider 인터페이스를 구현하도록 Application 클래스를 확장합니다.
  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" />