إذا كنت مصنّعًا أصليًا للأجهزة (OEM) أو كنت تحتفظ بعميل تحديث عبر الأثير (OTA) يتمتع بامتيازات، يمكنك منح التطبيقات التي تتطلّب مستوى أمان عاليًا على الجهاز إذن الوصول إلى تحديثات الأمان المعلّقة حتى تتمكّن من تقييم حالة أمان الجهاز بدقة. لفرض سياسات فعّالة تستند إلى مبدأ "عدم الثقة مطلقًا"، يجب أن تتمكّن التطبيقات من التحقّق ليس فقط من مستوى رمز تصحيح الأمان المثبَّت على الجهاز (مستوى رمز تصحيح أمان الجهاز أو DSPL)، ولكن أيضًا من تحديثات الأمان المتوفّرة والجاهزة للتثبيت (مستوى رمز تصحيح الأمان المتوفّر أو ASPL).
بما أنّ تطبيقات العميل التي لا تملك امتيازات لا يمكنها قراءة خصائص البرامج الثابتة مباشرةً أو فحص قواعد بيانات أدوات التحديث الخاصة أو طلب البحث من نقاط نهاية الخلفية الداخلية لمصنّع المعدات الأصلية، توفّر مكتبة AndroidX Security State Provider بنية موحّدة وآمنة للاتصال بين العمليات (IPC) تستخدمها أدوات تحديث العميل لمشاركة معلومات حول التحديثات المتاحة. من خلال تنفيذ UpdateInfoService في برنامج OTA، يمكنك نشر بيانات وصفية لنظام ASPL بدون الكشف عن عمليات الدمج الخاصة بالخادم الخلفي. في حين أنّ Google توفّر عمليات تنفيذ محدّثات مكوّنات النظام النمطية (Mainline) لأجهزة GMS، يمكن أيضًا للأجهزة غير التابعة لـ GMS نشر البيانات الوصفية لمستوى رمز تصحيح أمان Android (ASPL) لهذه المكوّنات النمطية للنظام.
نظرة عامة على البنية
يوضّح المخطّط التالي كيف تنشئ مكتبة AndroidX Security State Provider إطار عمل موحّدًا وآمنًا للتواصل البيني للعمليات (IPC) بين تطبيقات العميل غير المميزة وخدمات التحديث على الجهاز فقط:

نماذج تسليم البيانات
تتحقّق تطبيقات العميل من توفّر التحديثات من خلال استدعاء queryAllAvailableUpdates أو fetchAvailableSecurityPatchLevel. في الخلفية، تكتشف مكتبة
البرامج تلقائيًا جميع الخدمات المسجّلة التي توسّع فئة UpdateInfoService على الجهاز من تطبيقات النظام التي لديها إذن READ_PRIVILEGED_PHONE_STATE، وتربطها بها.
كما هو موضّح في المخطّط البياني السابق، تتوافق مكتبة security-state-provider مع نموذجين لعرض البيانات:
| نموذج التسليم | مشغّل المزامنة | استجابة العميل | حالات الاستخدام المقترَحة |
|---|---|---|---|
| نموذج الإرسال (المزامنة في الخلفية) | تتم مزامنة العمليات المجدوَلة التي تعمل في الخلفية (WorkManager أو JobScheduler) مع الخلفية وتكتب السجلات في UpdateInfoManager. تعرض خدمتك المحتوى دائمًا من ذاكرة التخزين المؤقت للقرص المحلي (shouldFetchUpdates() = false). |
يتم عرضها على الفور من ذاكرة التخزين المؤقت المحلية. | أدوات تحديث النظام عبر شبكة غير سلكية (OTA) من الشركة المصنّعة للجهاز، وأدوات تحديث المكوّنات النموذجية التي تتم مزامنتها في الخلفية |
| سحب النموذج (المزامنة عند الطلب) | تؤدي طلبات البحث الواردة من العميل بشأن عملية الاتصال بين العمليات (IPC) إلى استرجاع البيانات من الشبكة عندما تكون السجلات المخزّنة مؤقتًا قديمة (shouldFetchUpdates() = true). وتعمل ميزة دمج عمليات الإغلاق المتبادل وتقييد المعدل (shouldThrottle()) على حماية الخلفية من الارتفاعات المفاجئة. |
ينتظر عملية الجلب من الخلفية عندما تكون ذاكرة التخزين المؤقت قديمة. | أدوات تحديث OTA أحادية البنية من مصنّعي المعدات الأصلية بدون مشغّلي مزامنة مجدولة في الخلفية |
مقدّمو تحديثات متعددون
على أجهزة Android المخصّصة للإنتاج، تتوفّر عدة جهات مستقلة لتوفير التحديثات في الوقت نفسه. على سبيل المثال، تنشر Mainline معلومات التوفّر للمكوّنات النمطية (COMPONENT_SYSTEM_MODULES)، بينما ينشر برنامج OTA الخاص بمصنّع المعدات الأصلية التحديثات لصورة نظام التشغيل الأساسية (COMPONENT_SYSTEM).
يجب أن تسجّل خدمتك التحديثات للمكوّنات المحدّدة التي تديرها فقط. إذا نشر العديد من مقدّمي الخدمات على أحد الأجهزة تحديثات للمكوّن نفسه، ستقيِّم تطبيقات العميل أعلى مستوى تصحيح متاح (باستخدام fetchAvailableSecurityPatchLevel()) أو ستفحص سجلات UpdateInfo الفردية (باستخدام queryAllAvailableUpdates()) لأغراض التدقيق في المؤسسات. تأكَّد من أنّ خدمتك تنشر دائمًا التنسيق الأساسي للمكوّن (DateBasedSecurityPatchLevel لـ COMPONENT_SYSTEM).
دليل مفصّل لإعداد عميل التحديث
اتّبِع الخطوات التالية لدمج مكتبة AndroidX Security State Provider في برنامج التحديث والبدء في نشر معلومات حول توفّر تحديث الأمان لجهازك.
الخطوة 1: إضافة التبعيات
لتنفيذ موفّر تحديث، تأكَّد من أنّ مشروعك يتضمّن مستودع Google Maven، ثم أضِف مكتبة security-state-provider إلى ملف build.gradle.kts (Kotlin DSL) أو build.gradle (Groovy DSL) في الوحدة:
Kotlin
// Kotlin DSL (build.gradle.kts)
dependencies {
// Core provider library for OTA and system update clients
implementation("androidx.security:security-state-provider:1.0.0")
// Required to construct UpdateInfo and DateBasedSecurityPatchLevel records
implementation("androidx.security:security-state:1.1.0")
// Optional: Guava ListenableFuture support for Java implementations
implementation("androidx.concurrent:concurrent-futures:1.2.0")
implementation("com.google.guava:guava:33.0.0-android")
}
أنيق
// Groovy DSL (build.gradle)
dependencies {
// Core provider library for OTA and system update clients
implementation 'androidx.security:security-state-provider:1.0.0'
// Required to construct UpdateInfo and DateBasedSecurityPatchLevel records
implementation 'androidx.security:security-state:1.1.0'
// Optional: Guava ListenableFuture support for Java implementations
implementation 'androidx.concurrent:concurrent-futures:1.2.0'
implementation 'com.google.guava:guava:33.0.0-android'
}
الخطوة 2: تعريف خدمة التحديث في ملف البيان
يجب تعريف خدمتك في AndroidManifest.xml الخاص بتطبيقك باستخدام <intent-filter> مطابق androidx.security.state.provider.UPDATE_INFO_SERVICE. يجب تصدير الخدمة (android:exported="true") وضبطها كخدمة لمستخدم واحد (android:singleUser="true") حتى تتمكّن مكتبة العميل من ربطها عبر حدود العمليات والمستخدمين، خاصةً لملفات العمل:
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.android.updater">
<application>
<service
android:name=".MyUpdateInfoService"
android:exported="true"
android:singleUser="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="androidx.security.state.provider.UPDATE_INFO_SERVICE" />
</intent-filter>
</service>
</application>
</manifest>
إذا لم يتم تشغيل أداة التحديث بصفتها android.uid.system، عليك أيضًا تعريف الأذونات التالية في ملف البيان والتأكّد من إضافتها إلى قائمة السماح بالأذونات المميزة:
-
READ_PRIVILEGED_PHONE_STATE: مطلوبة لكي يثق العملاء في مقدّم الخدمة. -
INTERACT_ACROSS_USERS: مطلوب لـandroid:singleUser="true".
الخطوة 3: تنفيذ UpdateInfoService
لنشر حالة التحديث، يجب تنفيذ فئة UpdateInfoService وإنشاء سجلّات تحديث تطابق نموذج البيانات المتوقّع.
مواصفات نموذج بيانات UpdateInfo
سواء اخترت نموذج الدفع أو نموذج السحب، أنشئ سجلات UpdateInfo باستخدام UpdateInfo.Builder وفقًا للمواصفات التالية:
| اسم الحقل | Getter Method | نوع البيانات | متطلبات التحقّق من الصحة والتنسيق | الغرض والترميز الدلالي للنظام |
|---|---|---|---|---|
component |
getComponent() |
String (@Component) |
الثوابت الأساسية في SecurityPatchState: COMPONENT_SYSTEM أو COMPONENT_SYSTEM_MODULES أو COMPONENT_KERNEL |
تحدّد هذه السمة النظام الفرعي للبرامج أو البرامج الثابتة الذي يستهدفه هذا التحديث. |
securityPatchLevel |
getSecurityPatchLevel() |
SecurityPatchLevel |
يجب أن يكون مثيلاً من DateBasedSecurityPatchLevel (YYYY-MM-DD) أو VersionedSecurityPatchLevel (major.minor.patch)، أو يتم تحليله باستخدام SecurityPatchState.getComponentSecurityPatchLevel(). |
مستوى رمز تصحيح الأمان المستهدَف الذي سيتم تحقيقه بعد تثبيت هذا التحديث |
publishedDateMillis |
getPublishedDateMillis() |
long |
عدد الملّي ثانية منذ بدء حقبة Unix (System.currentTimeMillis())، ويجب أن يكون > 0. |
عندما أصبح التحديث متاحًا للمستخدمين، مثل وقت الإصدار عبر الهواء لا تستخدِم وقت تنزيل الحمولة أو تثبيتها. |
lastCheckTimeMillis |
getLastCheckTimeMillis() |
long |
عدد الملّي ثانية منذ بدء حقبة يونكس يجب أن تكون > 0. |
الطابع الزمني الذي تحقّق فيه مقدّم الخدمة من سجلّ التعديل هذا أو رصده أثناء المزامنة |
اختَر نموذج التسليم الذي يناسب بنية أداة التحديث من الخيارات التالية:
الخيار (أ): نموذج الدفع (مقترَح)
عندما يتحقّق عامل المزامنة في الخلفية من خادم OTA، تأكَّد من أنّ أي تحديث تم اكتشافه يؤدي إلى تقدّم مستوى تصحيح الجهاز الحالي واحتفظ به باستخدام UpdateInfoManager.registerUpdate()، أو اتّصِل بـ UpdateInfoManager.unregisterUpdate() إذا لم يكن هناك أي تحديث أمان متقدّم معلّق. يجب دائمًا استدعاء UpdateInfoManager.setLastCheckTimeMillis() في نهاية كل عملية مزامنة (حتى بعد استدعاء registerUpdate()، الذي يحفظ سجل UpdateInfo لكل مكون ولكن لا يعدّل الطابع الزمني العام لآخر عملية تحقّق الذي يتم عرضه للعملاء). يمكنك تنفيذ ذلك باستخدام
WorkManager CoroutineWorker في Kotlin أو Worker في Java:
Kotlin
import android.content.Context
import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlin.math.max
class OtaSyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val updateInfoManager = UpdateInfoManager(applicationContext)
val securityPatchState = SecurityPatchState(applicationContext)
val currentSpl = securityPatchState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
// 1. Fetch available update metadata from OEM backend
val latestUpdate = MyOtaClient.fetchLatestSystemUpdate()
val targetSplString = latestUpdate?.spl?.trim()
val targetSpl = if (!targetSplString.isNullOrEmpty()) {
DateBasedSecurityPatchLevel.fromString(targetSplString)
} else {
null
}
// 2. Defensively verify that target SPL is non-blank AND strictly newer than installed DSPL.
// If an update is a maintenance patch with no SPL increment (or if no update is available),
// unregister any stale cached record for this component.
if (latestUpdate != null && targetSpl != null && targetSpl > currentSpl) {
val updateInfo = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(targetSpl)
.setPublishedDateMillis(latestUpdate.releaseTimeMillis)
.setLastCheckTimeMillis(System.currentTimeMillis())
.build()
updateInfoManager.registerUpdate(updateInfo)
} else {
val clearTarget = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
updateInfoManager.unregisterUpdate(clearTarget)
}
// 3. Update global freshness timestamp (monotonic synchronization)
val currentCheckTime = System.currentTimeMillis()
val previousCheckTime = updateInfoManager.getLastCheckTimeMillis()
updateInfoManager.setLastCheckTimeMillis(max(previousCheckTime, currentCheckTime))
return Result.success()
}
}
Java
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.UpdateInfoManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class OtaSyncWorker extends Worker {
public OtaSyncWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
// In Java, pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
UpdateInfoManager updateInfoManager =
new UpdateInfoManager(getApplicationContext(), /* customSecurityState= */ null);
SecurityPatchState securityPatchState = new SecurityPatchState(getApplicationContext());
SecurityPatchLevel currentSpl =
securityPatchState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);
// 1. Fetch available update metadata from OEM backend
MyOtaUpdate latestUpdate = MyOtaClient.fetchLatestSystemUpdate();
String targetSplString = (latestUpdate != null && latestUpdate.getSpl() != null)
? latestUpdate.getSpl().trim()
: null;
DateBasedSecurityPatchLevel targetSpl =
!TextUtils.isEmpty(targetSplString)
? DateBasedSecurityPatchLevel.fromString(targetSplString)
: null;
// 2. Defensively verify that target SPL is non-blank AND strictly newer than installed DSPL.
// If an update is a maintenance patch with no SPL increment (or if no update is available),
// unregister any stale cached record for this component.
if (latestUpdate != null && targetSpl != null && targetSpl.compareTo(currentSpl) > 0) {
UpdateInfo updateInfo = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(targetSpl)
.setPublishedDateMillis(latestUpdate.getReleaseTimeMillis())
.setLastCheckTimeMillis(System.currentTimeMillis())
.build();
updateInfoManager.registerUpdate(updateInfo);
} else {
UpdateInfo clearTarget = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
updateInfoManager.unregisterUpdate(clearTarget);
}
// 3. Update global freshness timestamp (monotonic synchronization)
long currentCheckTime = System.currentTimeMillis();
long previousCheckTime = updateInfoManager.getLastCheckTimeMillis();
updateInfoManager.setLastCheckTimeMillis(Math.max(previousCheckTime, currentCheckTime));
return Result.success();
}
}
في النموذج المستند إلى الإشعارات، تحتفظ المهام التي تعمل في الخلفية بسجلات التعديل مباشرةً في
UpdateInfoManager. لتوجيه إطار العمل إلى عرض السجلات دائمًا من مساحة التخزين على القرص المحلي، عليك إلغاء shouldFetchUpdates() لعرض false من خلال توسيع UpdateInfoService في Kotlin أو ListenableFutureUpdateInfoService في Java:
Kotlin
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoService
class PushUpdateInfoService : UpdateInfoService() {
// Cache is populated out-of-band by background sync tasks
override fun shouldFetchUpdates(): Boolean = false
// Never invoked under normal flow because shouldFetchUpdates() returns false
override suspend fun fetchUpdates(): List<UpdateInfo> = emptyList()
}
Java
import androidx.annotation.NonNull;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collections;
import java.util.List;
public class PushUpdateInfoService extends ListenableFutureUpdateInfoService {
@Override
protected boolean shouldFetchUpdates() {
return false;
}
@NonNull
@Override
protected ListenableFuture<List<UpdateInfo>> fetchUpdatesAsync() {
return Futures.immediateFuture(Collections.emptyList());
}
}
الخيار (ب): نموذج السحب (عند الطلب)
في بنية مستندة إلى السحب، تعالج خدمتك طلبات إعادة التحميل عند الطلب التي يتم تشغيلها بواسطة تطبيقات العميل عندما تكون ذاكرة التخزين المؤقت المحلية قديمة.
للتعامل مع طلبات التحديث عند الطلب، يمكنك توسيع نطاق UpdateInfoService في Kotlin
(من خلال تنفيذ الدالة المعلقة fetchUpdates()) أو
ListenableFutureUpdateInfoService في Java (من خلال تنفيذ
fetchUpdatesAsync() التي تعرض ListenableFuture من Guava):
Kotlin
package com.example.android.updater
import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.security.state.provider.UpdateInfoService
import java.util.concurrent.TimeUnit
class MyUpdateInfoService : UpdateInfoService() {
// Manage local update records and check timestamps
private val updateInfoManager by lazy { UpdateInfoManager(this) }
override suspend fun fetchUpdates(): List<UpdateInfo> {
val currentSpl = SecurityPatchState(this)
.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
// 1. Execute network request to OTA backend
val response = MyOtaBackendClient.checkAvailableUpdates()
// 2. Defensively filter out blank or non-advancing SPLs and map to UpdateInfo objects
val validUpdates = response.updates
.mapNotNull { updateItem ->
val splString = updateItem.targetSpl?.trim()
if (splString.isNullOrEmpty()) return@mapNotNull null
val parsedSpl = DateBasedSecurityPatchLevel.fromString(splString)
if (parsedSpl > currentSpl) {
UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(parsedSpl)
.setPublishedDateMillis(updateItem.releaseTimestampMillis)
.setLastCheckTimeMillis(System.currentTimeMillis())
.build()
} else {
null
}
}
// 3. If no advancing SYSTEM update is available (or if a previously offered update was revoked),
// proactively unregister any cached record for this component.
if (validUpdates.isEmpty()) {
val clearTarget = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
updateInfoManager.unregisterUpdate(clearTarget)
}
return validUpdates
}
override fun shouldFetchUpdates(): Boolean {
// Enforce custom freshness threshold (for example, 4 hours instead of default 1 hour)
val lastCheckMillis = updateInfoManager.getLastCheckTimeMillis()
val dataAge = System.currentTimeMillis() - lastCheckMillis
return dataAge > TimeUnit.HOURS.toMillis(4)
}
}
Java
package com.example.android.updater;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import androidx.security.state.provider.UpdateInfoManager;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class MyUpdateInfoService extends ListenableFutureUpdateInfoService {
private UpdateInfoManager updateInfoManager;
@Override
public void onCreate() {
super.onCreate();
// Pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
updateInfoManager = new UpdateInfoManager(this, /* customSecurityState= */ null);
}
@NonNull
@Override
protected ListenableFuture<List<UpdateInfo>> fetchUpdatesAsync() {
try {
SecurityPatchLevel currentSpl = new SecurityPatchState(this)
.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);
MyOtaBackendResponse response = MyOtaBackendClient.checkAvailableUpdates();
List<UpdateInfo> updates = new ArrayList<>();
for (MyOtaUpdateItem item : response.getUpdates()) {
String trimmedSpl = (item.getTargetSpl() != null) ? item.getTargetSpl().trim() : null;
if (!TextUtils.isEmpty(trimmedSpl)) {
DateBasedSecurityPatchLevel parsedSpl =
DateBasedSecurityPatchLevel.fromString(trimmedSpl);
if (parsedSpl.compareTo(currentSpl) > 0) {
updates.add(new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(parsedSpl)
.setPublishedDateMillis(item.getReleaseTimestampMillis())
.setLastCheckTimeMillis(System.currentTimeMillis())
.build());
}
}
}
// If no advancing SYSTEM update is available (or if a previously offered update was revoked),
// proactively unregister any cached record for this component.
if (updates.isEmpty()) {
UpdateInfo clearTarget = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
updateInfoManager.unregisterUpdate(clearTarget);
}
return Futures.immediateFuture(updates);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
}
@Override
protected boolean shouldFetchUpdates() {
long lastCheckMillis = updateInfoManager.getLastCheckTimeMillis();
long dataAge = System.currentTimeMillis() - lastCheckMillis;
return dataAge > TimeUnit.HOURS.toMillis(4);
}
}
الخطوة 4: محو التحديثات التي تم تطبيقها بعد إعادة تشغيل الجهاز
على الرغم من أنّ UpdateInfoManager يزيل تلقائيًا التحديثات القديمة عند استدعاء registerUpdate()، لن يستدعي برنامج التحديث registerUpdate() مرة أخرى بعد انتهاء تثبيت تحديث عبر الأثير (OTA) إلى أن يحين موعد دورة المزامنة التالية المجدوَلة مع الخادم. لمنع تطبيقات العميل من اعتبار التحديث المثبَّت مسبقًا معلّقًا بعد إعادة التشغيل مباشرةً، استمع إلى ACTION_BOOT_COMPLETED واستدعِ UpdateInfoManager.unregisterUpdate() عند انتهاء تثبيت تحديث عبر الأثير (OTA) لمحو السجلّ من ذاكرة التخزين المؤقت المحلية. ويتم إجراء ذلك فقط عند الانتهاء من تثبيت أحد التحديثات، ما يمنع محو التحديثات المعلقة (التي لم يتم تثبيتها) بشكل غير مشروط عند كل عملية إعادة تشغيل عادية للجهاز. بما أنّ مفاتيح
UpdateInfoManager تعدّل السجلات حسب المكوّن، ما عليك سوى تحديد المكوّن المستهدف عند إنشاء عنصر
UpdateInfo لإلغاء التسجيل:
Kotlin
// Build target identifying the component to unregister
val target = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
// Unregister the update to remove it from disk cache
updateInfoManager.unregisterUpdate(target)
// Refresh last check timestamp to indicate up-to-date state
updateInfoManager.setLastCheckTimeMillis(System.currentTimeMillis())
Java
// Build target identifying the component to unregister
UpdateInfo target = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
// Unregister the update to remove it from disk cache
updateInfoManager.unregisterUpdate(target);
// Refresh last check timestamp to indicate up-to-date state
updateInfoManager.setLastCheckTimeMillis(System.currentTimeMillis());
الخطوة 5: التحقّق من عملية التكامل
أجرِ عمليات التحقّق التالية على جهاز Android أو محاكي باستخدام Android Debug Bridge (ADB) للتحقّق من صحة عملية الدمج التام بين الأطراف وتجنُّب المشاكل الشائعة التي قد تحدث عند نشر الشركات المصنّعة الأصلية (OEM):
التأكّد من أنّ العملاء يثقون بمقدّم الخدمة: تتجاهل تطبيقات العملاء أي مقدّم خدمة لا يملك
READ_PRIVILEGED_PHONE_STATE، حتى إذا كان مثبّتًا مسبقًا. تأكَّد من منح الإذن:adb shell dumpsys package <your_package_name> | grep "READ_PRIVILEGED_PHONE_STATE: granted=true"بعد ذلك، تأكَّد من إمكانية العثور على خدمتك ومن أنّها لا تتطلّب إذنًا. في ناتج خدمتك، ابحث عن
exported=trueوpermission=null:adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICEإذا لم يظهر الموفِّر للعميل، تحقَّق من logcat بحثًا عن
Ignoring untrusted update providerمن العلامةSecurityPatchState.التحقّق من حلّ الغرض في كل من "المستخدم 0" و"ملف العمل": تأكَّد من أنّ نظام التشغيل Android
PackageManagerيحلّ فلتر الأغراضUPDATE_INFO_SERVICEالذي تم تصديره في كل من المستخدم الأساسي (User 0) وأي ملف عمل نشط في Android Enterprise (مثلUser 10):adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICE adb shell pm query-services --user 10 -a androidx.security.state.provider.UPDATE_INFO_SERVICEالتحقّق من حالة الخدمة والسجلات المخزّنة مؤقتًا باستخدام
dumpsys: تتجاوزUpdateInfoServicedump()للإبلاغ عنGlobal Last CheckوShould Throttle(حالة أداة تحديد المعدّل) وCached Updates. بما أنّUpdateInfoServiceهي خدمة مرتبطة، وتتم إزالة الربط من العملاء فورًا بعد الاستعلام، تعرضdumpsys activity serviceالقيمة(nothing)عندما لا يكون أي عميل مرتبطًا. ابدأ الخدمة بشكل صريح قبل تشغيلdumpsys:adb shell am start-service -a androidx.security.state.provider.UPDATE_INFO_SERVICE <your_package_name>/.<service_class_name> adb shell dumpsys activity service <your_package_name>/.<service_class_name>مثال على ناتج التشخيص:
UpdateInfoService State: Active Requests: 0 Global Last Check: Thu Jan 01 12:00:00 UTC 2026 Should Throttle: false Cached Updates (1): - Component: SYSTEM SPL: 2026-01-01 Published: Thu Jan 01 00:00:00 UTC 2026 Last Checked: Thu Jan 01 12:00:00 UTC 2026تفعيل ربط العميل والتحقّق من نتائج بيانات القياس عن بُعد: من تطبيق اختبار غير مميّز (لا يملك أذونات توقيع النظام)، استدعِ
SecurityPatchState.queryAllAvailableUpdates(). إذا نفّذت عمليات معاودة الاتصال عن بُعد، تحقَّق مما يلي:- تأكَّد من أنّ العميل غير المميّز يتم ربطه بدون
SecurityExceptionويؤدي إلى تشغيلonClientConnected(packageName, callerUid). - بالنسبة إلى مقدّمي الخدمات الذين يستخدمون نموذج الدفع (
shouldFetchUpdates() == false): تأكَّد من أنّ سجلّاتonRequestCompleted(telemetry)تتضمّنUpdateFetchOutcome.CACHE_HIT(1) معfetchDurationMillis == 0في كل طلب بحث. - لموفّري نموذج السحب (
shouldFetchUpdates() == true): تأكَّد من أنّ سجلّاتonRequestCompleted(telemetry)تعرضUpdateFetchOutcome.FETCHED(3) عند طلب البحث الأوّلي عن ذاكرة التخزين المؤقت القديمة، يليهCACHE_HIT(1) عند طلبات البحث اللاحقة مباشرةً. (لإعادة ضبط أداة تحديد المعدّل المستمر لمدة ساعة واحدة بين عمليات تشغيل اختبارات نموذج السحب، شغِّلadb shell pm clear <your_package_name>.)
- تأكَّد من أنّ العميل غير المميّز يتم ربطه بدون
الإعدادات الاختيارية والمتقدّمة
سياسة التخزين المؤقت والحدّ من المعدّل
عندما يطلب العميل الحصول على آخر التعديلات، ينفّذ UpdateInfoService سير عمل قفل تم التحقّق منه مرّتين لتحقيق التوازن بين سرعة توفُّر البيانات وحِمل خادم الخلفية:

- المسار السريع (
shouldFetchUpdates()): بشكل تلقائي، تعرضshouldFetchUpdates()القيمةtrue(ما يشير إلى ذاكرة تخزين مؤقت قديمة) فقط عندما يكونlastCheckTimeMillisالعام أقدم من ساعة واحدة (TimeUnit.HOURS.toMillis(1)). وعندما تعرضshouldFetchUpdates()القيمةfalse، تعرض الخدمة على الفور السجلات المخزّنة مؤقتًا مع النتيجةUpdateFetchOutcome.CACHE_HITبدون الحصول على أقفال أو تنفيذ عمليات إدخال/إخراج على الشبكة. يمكنك إلغاءshouldFetchUpdates()لتخصيص سياسة التخزين المؤقت هذه. - المسار البطيء ودمج الطلبات: عندما تعرض
shouldFetchUpdates()القيمةtrue، تحصل الخدمة على قفل تبادلي داخلي لبرنامج فرعي وتُعيد تقييمshouldFetchUpdates()(تعرضUpdateFetchOutcome.COALESCEDإذا كان طلب متزامن قد أعاد تحميل البيانات من ذاكرة التخزين المؤقت أثناء انتظار القفل). - أداة تحديد المعدّل الثابت (
shouldThrottle()): لحماية البنية الأساسية من جهة الخلف من ارتفاع عدد الطلبات أو حالات الفشل المتكرّرة، تفرضshouldThrottle()فترة زمنية ثابتة لا تقل عن ساعة واحدة بين عمليات إعادة تشغيل التطبيق والجهاز. تسجّلUpdateInfoServiceكل محاولة قبل استدعاءfetchUpdates()، لذا إذا طرحتfetchUpdates()استثناءً (أي عرضتUpdateFetchOutcome.FAILEDبعد استدعاءonFetchFailed(e))، ستعرض الاستعلامات اللاحقة خلال فترة السماح البالغة 60 دقيقة بيانات احتياطية مخزّنة مؤقتًا مع النتيجةUpdateFetchOutcome.THROTTLED.
إمكانية تتبُّع البيانات والقياس عن بُعد والتشخيص
توفّر UpdateInfoService نقاط ربط مدمجة لإمكانية تتبُّع البيانات بهدف تتبُّع معدّل استخدام العملاء، ومراقبة وقت استجابة الاتصال بين العمليات (IPC)، وتسجيل أخطاء الخلفية بدون استخدام أدوات لإنشاء رموز AIDL الأساسية:
-
onRequestCompleted(telemetry): يتم استدعاء هذا الإجراء عند اكتمال كل عملية بحث عن التحديثات، مع عرض ملخّصUpdateCheckTelemetry. -
onClientConnected(packageName, callerUid): يتم استدعاؤه عندما يفتح عميل تم التحقّق منه جلسة. -
onClientDisconnected(packageName, callerUid): يتم استدعاؤها عندما يلغي أحد العملاء الربط أو تنتهي العملية. -
onFetchFailed(e): يتم استدعاؤه في حال حدوث استثناء أثناءfetchUpdates()، قبل أن تعرض الخدمة بيانات احتياطية مخزّنة مؤقتًا.
يمكنك إلغاء هاتين الدالتين في UpdateInfoService (Kotlin) أو
ListenableFutureUpdateInfoService (Java):
Kotlin
import androidx.security.state.provider.UpdateCheckTelemetry
import androidx.security.state.provider.UpdateFetchOutcome
import androidx.security.state.provider.UpdateInfoService
abstract class MonitoredUpdateInfoService : UpdateInfoService() {
override fun onRequestCompleted(telemetry: UpdateCheckTelemetry) {
val outcomeName = when (telemetry.outcome) {
UpdateFetchOutcome.CACHE_HIT -> "CACHE_HIT"
UpdateFetchOutcome.COALESCED -> "COALESCED"
UpdateFetchOutcome.FETCHED -> "FETCHED"
UpdateFetchOutcome.THROTTLED -> "THROTTLED"
UpdateFetchOutcome.FAILED -> "FAILED"
else -> "UNKNOWN"
}
MyAnalytics.logEvent("SECURITY_UPDATE_CHECK")
.addParam("outcome", outcomeName)
.addParam("total_duration_ms", telemetry.totalDurationMillis)
.addParam("lock_wait_ms", telemetry.lockWaitDurationMillis)
.addParam("processing_ms", telemetry.processingDurationMillis)
.addParam("fetch_duration_ms", telemetry.fetchDurationMillis)
.addParam("caller_uid", telemetry.callerUid)
.send()
}
override fun onClientConnected(packageName: String, callerUid: Int) {
// Track authenticated client sessions and adoption
MyMetrics.incrementCounter("client_connected", "package", packageName)
}
override fun onClientDisconnected(packageName: String, callerUid: Int) {
// Track session termination and cleanup resources
MyMetrics.incrementCounter("client_disconnected", "package", packageName)
}
override fun onFetchFailed(e: Exception) {
// Report exceptions caught during the update check workflow
MyCrashReporter.recordException(e)
}
}
Java
import androidx.annotation.NonNull;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import androidx.security.state.provider.UpdateCheckTelemetry;
import androidx.security.state.provider.UpdateFetchOutcome;
public abstract class MonitoredUpdateInfoService extends ListenableFutureUpdateInfoService {
@Override
protected void onRequestCompleted(@NonNull UpdateCheckTelemetry telemetry) {
String outcomeName;
switch (telemetry.getOutcome()) {
case UpdateFetchOutcome.CACHE_HIT: outcomeName = "CACHE_HIT"; break;
case UpdateFetchOutcome.COALESCED: outcomeName = "COALESCED"; break;
case UpdateFetchOutcome.FETCHED: outcomeName = "FETCHED"; break;
case UpdateFetchOutcome.THROTTLED: outcomeName = "THROTTLED"; break;
case UpdateFetchOutcome.FAILED: outcomeName = "FAILED"; break;
default: outcomeName = "UNKNOWN"; break;
}
MyAnalytics.logEvent("SECURITY_UPDATE_CHECK")
.addParam("outcome", outcomeName)
.addParam("total_duration_ms", telemetry.getTotalDurationMillis())
.addParam("lock_wait_ms", telemetry.getLockWaitDurationMillis())
.addParam("processing_ms", telemetry.getProcessingDurationMillis())
.addParam("fetch_duration_ms", telemetry.getFetchDurationMillis())
.addParam("caller_uid", telemetry.getCallerUid())
.send();
}
@Override
protected void onClientConnected(@NonNull String packageName, int callerUid) {
MyMetrics.incrementCounter("client_connected", "package", packageName);
}
@Override
protected void onClientDisconnected(@NonNull String packageName, int callerUid) {
MyMetrics.incrementCounter("client_disconnected", "package", packageName);
}
@Override
protected void onFetchFailed(@NonNull Exception e) {
MyCrashReporter.recordException(e);
}
}
نتائج القياس عن بُعد ومقاييس وقت الاستجابة
يقيس UpdateCheckTelemetry المدد المنقضية الرتيبة (SystemClock.elapsedRealtime()) ويعرض إحدى النتائج الخمس المحدّدة في UpdateFetchOutcome:
| الثابت الخاص بالنتيجة | @IntDef الرمز |
خصائص المقاييس التي يتم تسجيلها | الوصف وحالة النظام |
|---|---|---|---|
UpdateFetchOutcome.CACHE_HIT |
1 |
totalDurationMillis وprocessingDurationMillis وcallerUid |
يتم عرضها على الفور من ذاكرة التخزين المؤقت أو القرص المحلي على "المسار السريع" (يتم عرض shouldFetchUpdates() false). lockWaitDurationMillis وfetchDurationMillis هما 0. |
UpdateFetchOutcome.COALESCED |
2 |
totalDurationMillis وlockWaitDurationMillis وprocessingDurationMillis وcallerUid |
تم وضع الاستعلام في قائمة الانتظار خلف عملية إعادة تحميل نشطة أخرى، وعند الحصول على القفل، كانت البيانات حديثة. تجنُّب جلب الشبكة المكرّر (fetchDurationMillis هو 0) |
UpdateFetchOutcome.FETCHED |
3 |
totalDurationMillis، lockWaitDurationMillis، processingDurationMillis، fetchDurationMillis، callerUid |
تم تنفيذ مزامنة شبكة الخلفية بنجاح (اكتملت عملية fetchUpdates()). تمت كتابة السجلات الجديدة على القرص. |
UpdateFetchOutcome.THROTTLED |
4 |
totalDurationMillis وlockWaitDurationMillis وprocessingDurationMillis وcallerUid |
تم حظر الطلب من خلال أداة تحديد المعدّل (shouldThrottle() تم عرض true). تم عرض البيانات المخزّنة مؤقتًا بأمان للعميل (fetchDurationMillis هو 0). |
UpdateFetchOutcome.FAILED |
5 |
totalDurationMillis، lockWaitDurationMillis، processingDurationMillis، fetchDurationMillis، callerUid |
حدث خطأ أثناء البحث عن تحديث أو إرسال طلب شبكة. تمت معالجة الخطأ من خلال جدار الحماية الخاص بالاستثناءات، وتم تشغيل onFetchFailed(e)، وتم عرض المحتوى الاحتياطي المخزّن مؤقتًا. |
Advanced service broker hook: getCallerUid()
عندما يتصل أحد العملاء، تقيِّم UpdateInfoService تلقائيًا getCallerUid() في سلسلة Binder الأولية (قبل استدعاء Binder.clearCallingIdentity() قبل fetchUpdates())، وتتحقّق من ملكية الحزمة، وتمرّر معرّف المستخدم (UID) الخاص بالمتصل الذي تم التحقّق منه مباشرةً إلى onClientConnected() وonClientDisconnected() وtelemetry.callerUid في onRequestCompleted(telemetry).
بالنسبة إلى مكوّنات Android العادية <service>، ليس عليك استدعاء أو إلغاء getCallerUid(). يتم توفير طريقة protected open getCallerUid() (التي يتم تفويضها إلى Binder.getCallingUid() تلقائيًا) كخطاف إلغاء للتطبيقات المضيفة التي توجّه Binder IPC من خلال وسيط خدمة داخلي أو بنية وكيل، ما يتيح للفئة الفرعية عرض معرّف UID المنطقي للعميل بدلاً من معرّف UID الخاص بالوسيط.
مراجع إضافية
لمزيد من المعلومات حول حالة أمان النشر، يُرجى الاطّلاع على المراجع التالية:
الوثائق
- فهم حالة أمان الجهاز
- نشرات أمان Android
- مكوّنات النظام النموذجية
- حِزم تصحيح الأمان الإضافية
- ملاحظات إصدار Security State Provider 1.0.0
دليل API المرجعي
UpdateInfoServiceListenableFutureUpdateInfoServiceUpdateInfoManagerUpdateInfoUpdateInfo.BuilderUpdateCheckTelemetryUpdateFetchOutcomeSecurityPatchState