استخدام "مدير أدوات الاستشعار" لقياس الخطوات من جهاز جوّال

استخدِم "مدير أدوات الاستشعار" لملء بيانات الخطوات في تطبيق متوافق مع الأجهزة الجوّالة، كما هو موضَّح في هذا الدليل. لمزيد من المعلومات حول طريقة تصميم واجهة مستخدم لتطبيق التمارين الرياضية وإدارتها، يُرجى الرجوع إلى إنشاء تطبيق لياقة بدنية أساسي.

البدء

لبدء قياس خطوات عدّاد الخطوات الأساسية من جهازك الجوّال، عليك إضافة التبعيات إلى ملف 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")
}

إنشاء الخدمة التي تعمل في المقدّمة

في أحد تطبيقات اللياقة البدنية الأساسية، قد يتوفر لديك زر لبدء الأحداث وإيقافها من المستخدم لتتبع الخطوات.

احرص على مراعاة أفضل الممارسات المتعلقة بأجهزة الاستشعار. وعلى وجه الخصوص، يجب ألا يحتسب جهاز استشعار عدّاد الخطوات سوى الخطوات أثناء تسجيل أداة الاستشعار. من خلال ربط تسجيل جهاز الاستشعار بخدمة تعمل في المقدّمة، يتم تسجيل أداة الاستشعار طالما لزم الأمر، ويمكن أن تظل أداة الاستشعار مسجَّلة عندما لا يكون التطبيق في المقدّمة.

استخدِم المقتطف التالي لإلغاء تسجيل أداة الاستشعار في طريقة 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")
    }
}

إنشاء قاعدة بيانات لأحداث أدوات الاستشعار

قد يعرض تطبيقك شاشة يمكن للمستخدم من خلالها الاطّلاع على خطواته بمرور الوقت. لتوفير هذه الإمكانية في تطبيقك، استخدِم مكتبة استمرارية الغرفة.

ينشئ المقتطف التالي جدولاً يحتوي على مجموعة من قياسات عدد الخطوات، إلى جانب الوقت الذي وصل فيه تطبيقك إلى كل قياس:

@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 فئة الخطوة الجديدة، بحيث يمكنك تخزين الخطوات بمجرد قراءتها:

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.

لضبط الكائن 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" />