איך להשתמש במנהל החיישנים כדי למדוד שלבים ממכשיר נייד

אפשר להשתמש במרכז לניהול חיישנים כדי לאכלס נתוני צעדים באפליקציה לנייד, כמו שמתואר במדריך הזה. מידע נוסף על עיצוב וניהול של ממשק משתמש באפליקציית כושר זמין במאמר יצירת אפליקציית כושר בסיסית.

תחילת העבודה

כדי להתחיל למדוד את הצעדים באמצעות מד הצעדים הבסיסי מהמכשיר הנייד, צריך להוסיף את התלויות לקובץ build.gradle של מודול האפליקציה. מוודאים שמשתמשים בגרסאות העדכניות ביותר של התלויות. בנוסף, כשמרחיבים את התמיכה באפליקציה לגורמי צורה אחרים, כמו Wear OS, צריך להוסיף את התלות שגורמי הצורה האלה דורשים.

בהמשך מפורטות כמה דוגמאות לתלות בממשק המשתמש. רשימה מלאה זמינה במדריך רכיבי ממשק משתמש.

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. קבלת אובייקט SensorManager מ-getSystemService().
  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")
}

יצירת שירות בחזית

באפליקציית כושר בסיסית, יכול להיות שיהיה לחצן לקבלת אירועי התחלה ועצירה מהמשתמש לצורך מעקב אחר צעדים.

חשוב לפעול לפי השיטות המומלצות לשימוש בחיישנים. באופן ספציפי, חיישן מד הצעדים צריך לספור צעדים רק בזמן שרכיב ה-listener של החיישן רשום. כשמשייכים את רישום החיישן לשירות שפועל בחזית, החיישן נרשם כל עוד יש בו צורך, והוא יכול להישאר רשום גם כשהאפליקציה לא פועלת בחזית.

כדי לבטל את הרישום של החיישן בשיטה 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 persistence.

קטע הקוד הבא יוצר טבלה שמכילה קבוצה של מדידות של מספר הצעדים, לצד השעה שבה האפליקציה ניגשה לכל מדידה:

@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. מרחיבים את המחלקה 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" />