Если вы производитель оригинального оборудования (OEM) или поддерживаете клиент беспроводного обновления (OTA) с особыми правами, вы можете предоставить приложениям, требующим повышенной безопасности, доступ к информации о предстоящих обновлениях системы безопасности. Это позволит им точно оценивать уровень защиты устройства. Чтобы применять надежные правила нулевого доверия, приложения должны иметь возможность проверять не только уровень исправления, установленный на устройстве (уровень исправления системы безопасности устройства, DSPL), но и доступные и готовые к установке обновления системы безопасности (доступный уровень исправления системы безопасности, ASPL).
Поскольку клиентские приложения без привилегий не могут напрямую считывать свойства встроенного ПО, проверять частные базы данных обновлений или запрашивать внутренние конечные точки OEM-производителя, библиотека AndroidX Security State Provider предоставляет стандартизированную и безопасную архитектуру межпроцессного взаимодействия (IPC), которую клиенты обновлений используют для обмена информацией о доступных обновлениях. Внедрив UpdateInfoService в клиент OTA-обновлений, вы сможете публиковать метаданные ASPL для системы, не раскрывая собственные интеграции с внутренними системами. Google предоставляет реализацию для модульных системных компонентов (Mainline) для устройств с сервисами Google Play, но устройства без сервисов Google Play также могут публиковать метаданные ASPL для этих модульных системных компонентов.
Обзор архитектуры
На диаграмме ниже показано, как библиотека AndroidX Security State Provider создает стандартизированную безопасную структуру IPC между клиентскими приложениями без прав доступа и службами обновления на устройстве.

Модели передачи данных
Клиентские приложения запрашивают информацию о доступности обновлений, вызывая метод queryAllAvailableUpdates или fetchAvailableSecurityPatchLevel. Клиентская библиотека автоматически обнаруживает и подключается ко всем зарегистрированным сервисам, которые расширяют класс UpdateInfoService на устройстве из системных приложений, имеющих разрешение READ_PRIVILEGED_PHONE_STATE.
Как показано на схеме выше, библиотека security-state-provider поддерживает две модели передачи данных:
| Модель доставки | Триггер синхронизации | Ответ клиента | Рекомендуемые варианты использования |
|---|---|---|---|
| Модель Push (фоновая синхронизация) | Запланированные фоновые процессы (WorkManager или JobScheduler) синхронизируются с вашей серверной частью и записывают данные в UpdateInfoManager. Ваш сервис всегда использует локальный кеш диска (shouldFetchUpdates() = false). |
Сразу же показывается из локального кеша. | Системные беспроводные обновления от производителя и обновления модульных компонентов, синхронизируемые в фоновом режиме. |
| Извлечь модель (синхронизация по запросу) | Входящие клиентские запросы IPC запускают сетевое получение, когда записи в кеше устарели (shouldFetchUpdates() = true). Объединение мьютексов и ограничение скорости (shouldThrottle()) защищают серверную часть от пиковых нагрузок. |
Ожидает получения данных от внутреннего сервиса, когда кеш устарел. | Монолитные программы для обновления ПО по беспроводной сети от производителей устройств без запланированных фоновых синхронизаций. |
Несколько поставщиков обновлений
На устройствах Android, предназначенных для использования, одновременно работают несколько независимых поставщиков обновлений. Например, Mainline публикует информацию о доступности модульных компонентов (COMPONENT_SYSTEM_MODULES), а клиент OTA от производителя оригинального оборудования – обновления для основного образа ОС (COMPONENT_SYSTEM).
Сервису нужно регистрировать обновления только для тех компонентов, которыми он управляет. Если несколько поставщиков на устройстве публикуют обновления для одного и того же компонента, клиентские приложения оценивают самый высокий доступный уровень исправлений (с помощью fetchAvailableSecurityPatchLevel()) или проверяют отдельные записи UpdateInfo (с помощью queryAllAvailableUpdates()) для корпоративного аудита. Убедитесь, что ваш сервис всегда публикует канонический формат для компонента (DateBasedSecurityPatchLevel для COMPONENT_SYSTEM).
Пошаговое руководство по регистрации клиента для обновлений
Чтобы интегрировать библиотеку AndroidX Security State Provider в клиент обновлений и начать публиковать информацию о доступности обновлений безопасности для устройства, выполните следующие действия:
Шаг 1. Добавьте зависимости
Чтобы реализовать поставщика обновлений, убедитесь, что в вашем проекте есть репозиторий 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 в соответствии со следующими спецификациями:
| Название поля | Метод получения | Тип данных | Требования к формату и проверке | Назначение и семантика системы |
|---|---|---|---|---|
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. |
Когда обновление стало доступно пользователям, например время выпуска OTA. Не используйте время скачивания или установки полезной нагрузки. |
lastCheckTimeMillis |
getLastCheckTimeMillis() |
long |
Миллисекунды с начала эпохи Unix. Необходимое значение: > 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(), возвращающую 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(). Однако после установки OTA-обновления ваш установщик не будет вызывать registerUpdate() до следующего запланированного цикла синхронизации с сервером. Чтобы клиентские приложения не считали уже установленное обновление ожидающим сразу после перезагрузки, отслеживайте 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.Проверьте разрешение интентов в профиле пользователя 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: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. - Для поставщиков с моделью извлечения (
shouldFetchUpdates() == true): убедитесь, что журналыonRequestCompleted(telemetry)UpdateFetchOutcome.FETCHED(3) в исходном запросе к устаревшему кешу, а затемCACHE_HIT(1) в последующих запросах. (Чтобы сбросить ограничение частоты, действующее в течение часа, между тестовыми запусками модели Pull, выполните команду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) и возвращена резервная копия из кеша. |
Расширенный хук брокера сервисов: 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