אם אתם יצרני ציוד מקורי (OEM) או מנהלים לקוח עדכונים דרך האוויר (OTA) עם הרשאות מיוחדות, אתם יכולים לתת לאפליקציות רגישות לאבטחה במכשיר גישה לעדכוני אבטחה בהמתנה, כדי שהן יוכלו להעריך בצורה מדויקת את מצב האבטחה של המכשיר. כדי לאכוף מדיניות חזקה של אפס אמון, האפליקציות צריכות להיות מסוגלות לאמת לא רק את רמת התיקון שהותקנה במכשיר (רמת תיקון האבטחה במכשיר, או DSPL), אלא גם אילו עדכוני אבטחה זמינים ומוכנים להתקנה (רמת תיקון האבטחה הזמינה, או ASPL).
אפליקציות לקוח ללא הרשאות לא יכולות לקרוא ישירות מאפיינים של קושחה, לבדוק מסדי נתונים פרטיים של כלי עדכון או לשלוח שאילתות לנקודות קצה פנימיות של ספקי OEM. לכן, ספריית AndroidX Security State Provider מספקת ארכיטקטורה סטנדרטית ומאובטחת של תקשורת בין תהליכים (IPC) שמשמשת את לקוחות העדכון לשיתוף מידע על עדכונים זמינים. אם מטמיעים UpdateInfoService בלקוח העדכון של OTA, אפשר לפרסם מטא-נתונים של ASPL למערכת בלי לחשוף שילובי קצה עורפי קנייניים. Google מספקת הטמעה של עדכונים לרכיבי מערכת מודולריים (Mainline) למכשירי GMS, אבל גם מכשירים שאינם GMS יכולים לפרסם מטא-נתונים של ASPL לרכיבי המערכת המודולריים האלה.
סקירה כללית של הארכיטקטורה
בתרשים הבא מוצג איך ספריית AndroidX Security State Provider יוצרת מסגרת IPC מאובטחת וסטנדרטית בין אפליקציות לקוח ללא הרשאות לבין שירותי עדכון במכשיר:

מודלים של העברת נתונים
אפליקציות לקוח שולחות שאילתות כדי לבדוק אם יש עדכונים זמינים באמצעות הקריאות queryAllAvailableUpdates או fetchAvailableSecurityPatchLevel. מתחת לפני השטח, ספריית הלקוח מגלה באופן אוטומטי את כל השירותים הרשומים שמרחיבים את המחלקה UpdateInfoService במכשיר מאפליקציות מערכת שמחזיקות בהרשאה READ_PRIVILEGED_PHONE_STATE, וקושרת אליהם.
כפי שמוצג בתרשים שלמעלה, ספריית security-state-provider תומכת בשני מודלים של העברת נתונים:
| מודל מסירה | טריגר סנכרון | תגובת הלקוח | תרחישי שימוש מומלצים |
|---|---|---|---|
| מודל דחיפה (סנכרון ברקע) | עובדים ברקע שמתוזמנים (WorkManager או JobScheduler) מסתנכרנים עם ה-Backend וכותבים רשומות ל-UpdateInfoManager. השירות שלכם תמיד פועל ממטמון מקומי בדיסק (shouldFetchUpdates() = false). |
התגובה מוגשת באופן מיידי מהמטמון המקומי. | עדכוני OTA של מערכת OEM ועדכונים של רכיבים מודולריים שמסונכרנים ברקע. |
| מודל משיכה (סנכרון לפי דרישה) | שאילתות IPC של לקוחות נכנסות מפעילות אחזור מהרשת כשנתונים במטמון לא עדכניים (shouldFetchUpdates() = true). איחוד של mutex והגבלת קצב (shouldThrottle()) מגנים על הקצה העורפי מפני עליות פתאומיות. |
ההמתנה לאחזור מהקצה העורפי כשהמטמון לא עדכני. | כלי עדכון OTA מונוליטיים של יצרני ציוד מקורי (OEM) ללא עובדי סנכרון ברקע מתוזמנים. |
ספקי עדכונים מרובים
במכשירי Android בייצור, כמה ספקי עדכונים עצמאיים פועלים בו-זמנית. לדוגמה, Mainline מפרסם זמינות לרכיבים מודולריים (COMPONENT_SYSTEM_MODULES), בעוד לקוח ה-OTA של יצרן הציוד המקורי מפרסם עדכונים לתמונה הראשית של מערכת ההפעלה (COMPONENT_SYSTEM).
השירות צריך לרשום עדכונים רק לרכיבים הספציפיים שהוא מנהל. אם כמה ספקים במכשיר מפרסמים עדכונים לאותו רכיב, אפליקציות הלקוח מעריכות את רמת התיקון הגבוהה ביותר שזמינה (באמצעות fetchAvailableSecurityPatchLevel()) או בודקות רשומות נפרדות של UpdateInfo (באמצעות queryAllAvailableUpdates()) לצורך ביקורת ארגונית. מוודאים שהשירות שלכם תמיד מפרסם את הפורמט הקנוני של הרכיב (DateBasedSecurityPatchLevel עבור COMPONENT_SYSTEM).
מדריך מפורט להפעלת לקוח העדכון
כדי לשלב את ספריית AndroidX Security State Provider בלקוח העדכון ולהתחיל לפרסם את הזמינות של עדכון האבטחה במכשיר, צריך לבצע את השלבים הבאים.
שלב 1: מוסיפים יחסי תלות
כדי להטמיע ספק עדכונים, מוודאים שהפרויקט כולל את מאגר ה-Maven של Google, ואז מוסיפים את הספרייה 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
לא משנה אם בוחרים במודל Push או במודל Pull, צריך ליצור רשומות של 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 |
אלפיות השנייה מאז ראשית זמן יוניקס (System.currentTimeMillis()). הערך חייב להיות > 0. |
מתי העדכון הפך לזמין למשתמשים, למשל שעת ההשקה של OTA. אל תשתמשו בזמן ההורדה או ההתקנה של מטען הייעודי (payload). |
lastCheckTimeMillis |
getLastCheckTimeMillis() |
long |
אלפיות השנייה מאז ראשית זמן יוניקס (Unix epoch). חייב להיות > 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. כדי להנחות את מסגרת ה-SDK להציג תמיד רשומות מאחסון דיסק מקומי, צריך לבטל את השיטה 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() שמחזירה Guava ListenableFuture):
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: אימות השילוב
כדי לוודא שהשילוב מקצה לקצה תקין ולמנוע בעיות נפוצות בהטמעה של יצרני ציוד מקורי (OEM), מריצים את הבדיקות הבאות במכשיר Android או באמולטור באמצעות Android Debug Bridge (ADB):
מוודאים שהלקוחות סומכים על הספק: אפליקציות לקוח מתעלמות מכל ספק שלא מחזיק ב-
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.אימות של פתרון ה-Intent גם במשתמש 0 וגם בפרופיל העבודה: מוודאים שמערכת ההפעלה Android
PackageManagerפותרת את מסנן ה-Intent המיוצא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: UpdateInfoServiceמחליף אתdump()כדי לדווח על 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). - לספקים של מודל Push (
shouldFetchUpdates() == false): מוודאים שיומניonRequestCompleted(telemetry)UpdateFetchOutcome.CACHE_HIT(1) עםfetchDurationMillis == 0בכל שאילתה. - לספקים של מודל Pull (
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, השירות מקבל mutex פנימי של שגרת המשך (coroutine) ומעריך מחדש אתshouldFetchUpdates()(מחזירUpdateFetchOutcome.COALESCEDאם בקשה מקבילה כבר רעננה את המטמון בזמן ההמתנה לנעילה). - הגבלת קצב מתמשכת (
shouldThrottle()): כדי להגן על תשתית ה-Backend מפני פרצי שאילתות או כשלים חוזרים,shouldThrottle()אוכפת מרווח מינימלי מתמשך של שעה בין הפעלות מחדש של האפליקציה והמכשיר. UpdateInfoServiceמתעד כל ניסיון לפני הפעלתfetchUpdates(), כך שאםfetchUpdates()יוצר חריגה (מחזירUpdateFetchOutcome.FAILEDאחרי הפעלתonFetchFailed(e)), שאילתות עוקבות במהלך 60 הדקות הבאות מחזירות בצורה תקינה נתוני גיבוי במטמון עם התוצאהUpdateFetchOutcome.THROTTLED.
ניראות, טלמטריה ואבחון
UpdateInfoService מספקת ווים מובנים של יכולת צפייה כדי לעקוב אחרי אימוץ הלקוח, לעקוב אחרי השהיה של IPC ולרשום ביומן שגיאות של קצה העורפי בלי להשתמש ב-stubs של 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:
| Outcome Constant | @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 |
הסנכרון של רשת ה-Backend בוצע בהצלחה (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()) מסופקת כ-override hook לאפליקציות מארחות שמנתבות את Binder IPC דרך מתווך שירות פנימי או ארכיטקטורת proxy, ומאפשרת למחלקת המשנה להחזיר את ה-UID הלוגי של הלקוח במקום ה-UID של המתווך.
מקורות מידע נוספים
מידע נוסף על פרסום מצב האבטחה זמין במקורות המידע הבאים:
מאמרי עזרה
- הסבר על מצב האבטחה של המכשיר
- חדשות האבטחה של Android
- רכיבים מודולריים של המערכת
- תיקוני אבטחה משלימים
- הערות לגבי הגרסה Security State Provider 1.0.0
הפניית API
UpdateInfoServiceListenableFutureUpdateInfoServiceUpdateInfoManagerUpdateInfoUpdateInfo.BuilderUpdateCheckTelemetryUpdateFetchOutcomeSecurityPatchState