Как опубликовать данные о безопасности устройства

Если вы производитель оригинального оборудования (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 между клиентскими приложениями без прав доступа и службами обновления на устройстве.

Библиотека AndroidX Security State Provider создает стандартизированную безопасную структуру межпроцессного взаимодействия между клиентскими приложениями без привилегий и службами обновления на устройстве.

Модели передачи данных

Клиентские приложения запрашивают информацию о доступности обновлений, вызывая метод 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):

  1. Убедитесь, что клиенты доверяют вашему поставщику. Клиентские приложения игнорируют любого поставщика, у которого нет сертификата 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.

  2. Проверьте разрешение интентов в профиле пользователя 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
    
  3. Проверьте статус сервиса и записи в кеше с помощью команды 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
    
  4. Запустите привязку клиента и проверьте результаты телеметрии. В непривилегированном тестовом приложении (без разрешений на подпись системы) вызовите 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 выполняет рабочий процесс блокировки с двойной проверкой, чтобы сбалансировать актуальность данных и нагрузку на внутренний сервер:

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:

Переопределите эти обратные вызовы в 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 брокера.

Дополнительные ресурсы

Подробнее о публикации статуса безопасности рассказывается в следующих статьях:

Документация

Документация по API