Se você for um fabricante de equipamento original (OEM) ou mantiver um cliente de atualização over-the-air (OTA) privilegiado, poderá dar aos apps sensíveis à segurança no dispositivo visibilidade das atualizações de segurança pendentes para que eles possam avaliar com precisão a postura de segurança do dispositivo. Para aplicar políticas robustas de confiança zero, os apps precisam verificar não apenas o nível do patch instalado no dispositivo (nível do patch de segurança do dispositivo ou DSPL, na sigla em inglês), mas também quais atualizações de segurança estão disponíveis e prontas para instalação (nível do patch de segurança disponível ou ASPL, na sigla em inglês).
Como os apps cliente sem privilégios não podem ler diretamente as propriedades
do firmware, inspecionar bancos de dados particulares do atualizador ou consultar endpoints
internos do back-end do OEM, a biblioteca AndroidX Security State Provider oferece uma
arquitetura de comunicação entre processos (IPC) padronizada e segura que os clientes
de atualização usam para compartilhar informações sobre atualizações disponíveis. Ao implementar um
UpdateInfoService no cliente de atualização OTA, é possível publicar metadados
ASPL para o sistema sem expor integrações de back-end proprietárias. Embora o Google forneça a implementação de atualizadores de componentes modulares do sistema (Mainline) para dispositivos com GMS, os dispositivos sem GMS também podem publicar metadados ASPL para esses componentes.
Visão geral da arquitetura
O diagrama a seguir ilustra como a biblioteca AndroidX Security State Provider estabelece uma estrutura IPC padronizada e segura entre apps cliente sem privilégios e serviços de atualização no dispositivo:

Modelos de fornecimento de dados
Os apps clientes consultam a disponibilidade de atualizações chamando
queryAllAvailableUpdates ou
fetchAvailableSecurityPatchLevel. Por baixo dos panos, a biblioteca
de cliente descobre e vincula automaticamente todos os serviços registrados que
estendem a classe UpdateInfoService no dispositivo de apps do sistema que
têm a permissão READ_PRIVILEGED_PHONE_STATE.
Conforme ilustrado no diagrama anterior, a biblioteca security-state-provider
aceita dois modelos de entrega de dados:
| Modelo de entrega | Acionador de sincronização | Resposta do cliente | Casos de uso recomendados |
|---|---|---|---|
| Modelo push (sincronização em segundo plano) | Os workers em segundo plano programados (WorkManager ou JobScheduler) são sincronizados com seu back-end e gravam registros em UpdateInfoManager. Seu serviço sempre atende do cache em disco local (shouldFetchUpdates() = false). |
Servido imediatamente do cache local. | Atualizadores OTA do sistema OEM e atualizadores de componentes modulares sincronizados em segundo plano. |
| Modelo pull (sincronização sob demanda) | As consultas de IPC do cliente recebidas acionam uma busca de rede quando os registros em cache estão desatualizados (shouldFetchUpdates() = true). A fusão de mutex e a limitação de taxa (shouldThrottle()) protegem seu back-end contra picos. |
Espera a busca do back-end quando o cache está desatualizado. | Atualizadores OTA OEM monolíticos sem workers de sincronização em segundo plano programados. |
Vários provedores de atualizações
Em dispositivos Android de produção, vários provedores de atualização independentes coexistem
simultaneamente. Por exemplo, o Mainline publica a disponibilidade de componentes
modulares (COMPONENT_SYSTEM_MODULES), enquanto o cliente OTA do OEM
publica atualizações para a imagem principal do SO (COMPONENT_SYSTEM).
Seu serviço só precisa registrar atualizações para os componentes específicos que ele
gerencia. Se vários provedores em um dispositivo publicarem atualizações para o mesmo
componente, os apps clientes vão avaliar o nível de patch mais alto disponível (usando
fetchAvailableSecurityPatchLevel()) ou inspecionar registros individuais de
UpdateInfo (usando queryAllAvailableUpdates()) para
auditoria empresarial. Verifique se o serviço sempre publica o formato
canônico do seu componente (DateBasedSecurityPatchLevel para
COMPONENT_SYSTEM).
Guia explicativo para integrar seu cliente de atualização
Siga estas etapas para integrar a biblioteca do provedor de estado de segurança do AndroidX ao seu cliente de atualização e comece a publicar a disponibilidade de atualização de segurança do dispositivo.
Etapa 1: adicionar dependências
Para implementar um provedor de atualizações, verifique se o projeto inclui o repositório Maven do Google e adicione a biblioteca security-state-provider ao arquivo build.gradle.kts (Kotlin DSL) ou build.gradle (Groovy DSL) do módulo:
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
// 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'
}
Etapa 2: declarar o serviço de atualização no manifesto
Declare o serviço no AndroidManifest.xml do app com um
<intent-filter> correspondente
androidx.security.state.provider.UPDATE_INFO_SERVICE. O serviço precisa ser
exportado (android:exported="true") e configurado como um serviço de usuário único
(android:singleUser="true") para que a biblioteca de cliente possa se vincular a ele em
processos e limites de usuários, principalmente para perfis de trabalho:
<!-- 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>
Se o atualizador não for executado como android.uid.system, declare também as seguintes
permissões no manifesto e adicione-as à lista de permissões
privilegiadas:
READ_PRIVILEGED_PHONE_STATE: necessário para que os clientes confiem no seu provedor.INTERACT_ACROSS_USERS: obrigatório paraandroid:singleUser="true".
Etapa 3: implementar UpdateInfoService
Para publicar o status da atualização, implemente a classe UpdateInfoService
e crie registros de atualização que correspondam ao modelo de dados esperado.
Especificação do modelo de dados UpdateInfo
Se você escolher o modelo push ou pull, crie registros UpdateInfo usando UpdateInfo.Builder de acordo com a seguinte especificação:
| Nome do campo | Método getter | Tipo de dado | Requisitos de validação e formato | Objetivo e semântica do sistema |
|---|---|---|---|---|
component |
getComponent() |
String (@Component) |
Constantes canônicas em SecurityPatchState: COMPONENT_SYSTEM, COMPONENT_SYSTEM_MODULES ou COMPONENT_KERNEL. |
Identifica o subsistema de software ou firmware a que esta atualização se destina. |
securityPatchLevel |
getSecurityPatchLevel() |
SecurityPatchLevel |
Precisa ser uma instância de DateBasedSecurityPatchLevel (YYYY-MM-DD) ou VersionedSecurityPatchLevel (major.minor.patch) ou analisada usando SecurityPatchState.getComponentSecurityPatchLevel(). |
O nível do patch de segurança de destino que será alcançado quando esta atualização for instalada. |
publishedDateMillis |
getPublishedDateMillis() |
long |
Milissegundos desde a época Unix (System.currentTimeMillis()). Precisa ser > 0. |
Quando a atualização foi disponibilizada aos usuários, como o horário de lançamento da OTA. Não use o tempo de download ou instalação do payload. |
lastCheckTimeMillis |
getLastCheckTimeMillis() |
long |
Milissegundos desde a época Unix. Precisa ser > 0. |
Carimbo de data/hora de quando o provedor verificou ou descobriu esse registro de atualização durante a sincronização. |
Escolha o modelo de entrega que se adapta à arquitetura do seu atualizador entre as seguintes opções:
Opção A: modelo de push (recomendado)
Quando o worker de sincronização em segundo plano verificar seu servidor OTA, valide se alguma
atualização descoberta avança o nível de patch atual do dispositivo e persista usando
UpdateInfoManager.registerUpdate() ou chame
UpdateInfoManager.unregisterUpdate() se não houver uma atualização de segurança pendente
de avanço. Sempre chame UpdateInfoManager.setLastCheckTimeMillis() no final de cada sincronização, mesmo depois de chamar registerUpdate(), que persiste o registro UpdateInfo por componente, mas não atualiza o carimbo de data/hora global da última verificação retornado aos clientes. Você pode implementar isso com um
WorkManager CoroutineWorker em Kotlin ou Worker em 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();
}
}
Em um modelo baseado em push, as tarefas em segundo plano persistem registros de atualização diretamente no
UpdateInfoManager. Para instruir o framework a sempre veicular registros do
armazenamento em disco local, substitua shouldFetchUpdates() para retornar false
estendendo UpdateInfoService em Kotlin ou
ListenableFutureUpdateInfoService em 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());
}
}
Opção B: modelo pull (sob demanda)
Em uma arquitetura baseada em pull, seu serviço processa solicitações de atualização sob demanda acionadas por apps clientes quando o cache local está desatualizado.
Para processar consultas de atualização sob demanda, estenda UpdateInfoService em Kotlin
(implementando a função de suspensão fetchUpdates()) ou
ListenableFutureUpdateInfoService em Java (implementando
fetchUpdatesAsync() retornando um ListenableFuture do Guava):
Kotlin
package com.example.android.updater
import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.security.state.provider.UpdateInfoService
import java.util.concurrent.TimeUnit
class MyUpdateInfoService : UpdateInfoService() {
// Manage local update records and check timestamps
private val updateInfoManager by lazy { UpdateInfoManager(this) }
override suspend fun fetchUpdates(): List<UpdateInfo> {
val currentSpl = SecurityPatchState(this)
.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
// 1. Execute network request to OTA backend
val response = MyOtaBackendClient.checkAvailableUpdates()
// 2. Defensively filter out blank or non-advancing SPLs and map to UpdateInfo objects
val validUpdates = response.updates
.mapNotNull { updateItem ->
val splString = updateItem.targetSpl?.trim()
if (splString.isNullOrEmpty()) return@mapNotNull null
val parsedSpl = DateBasedSecurityPatchLevel.fromString(splString)
if (parsedSpl > currentSpl) {
UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(parsedSpl)
.setPublishedDateMillis(updateItem.releaseTimestampMillis)
.setLastCheckTimeMillis(System.currentTimeMillis())
.build()
} else {
null
}
}
// 3. If no advancing SYSTEM update is available (or if a previously offered update was revoked),
// proactively unregister any cached record for this component.
if (validUpdates.isEmpty()) {
val clearTarget = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
updateInfoManager.unregisterUpdate(clearTarget)
}
return validUpdates
}
override fun shouldFetchUpdates(): Boolean {
// Enforce custom freshness threshold (for example, 4 hours instead of default 1 hour)
val lastCheckMillis = updateInfoManager.getLastCheckTimeMillis()
val dataAge = System.currentTimeMillis() - lastCheckMillis
return dataAge > TimeUnit.HOURS.toMillis(4)
}
}
Java
package com.example.android.updater;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import androidx.security.state.provider.UpdateInfoManager;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class MyUpdateInfoService extends ListenableFutureUpdateInfoService {
private UpdateInfoManager updateInfoManager;
@Override
public void onCreate() {
super.onCreate();
// Pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
updateInfoManager = new UpdateInfoManager(this, /* customSecurityState= */ null);
}
@NonNull
@Override
protected ListenableFuture<List<UpdateInfo>> fetchUpdatesAsync() {
try {
SecurityPatchLevel currentSpl = new SecurityPatchState(this)
.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);
MyOtaBackendResponse response = MyOtaBackendClient.checkAvailableUpdates();
List<UpdateInfo> updates = new ArrayList<>();
for (MyOtaUpdateItem item : response.getUpdates()) {
String trimmedSpl = (item.getTargetSpl() != null) ? item.getTargetSpl().trim() : null;
if (!TextUtils.isEmpty(trimmedSpl)) {
DateBasedSecurityPatchLevel parsedSpl =
DateBasedSecurityPatchLevel.fromString(trimmedSpl);
if (parsedSpl.compareTo(currentSpl) > 0) {
updates.add(new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(parsedSpl)
.setPublishedDateMillis(item.getReleaseTimestampMillis())
.setLastCheckTimeMillis(System.currentTimeMillis())
.build());
}
}
}
// If no advancing SYSTEM update is available (or if a previously offered update was revoked),
// proactively unregister any cached record for this component.
if (updates.isEmpty()) {
UpdateInfo clearTarget = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
updateInfoManager.unregisterUpdate(clearTarget);
}
return Futures.immediateFuture(updates);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
}
@Override
protected boolean shouldFetchUpdates() {
long lastCheckMillis = updateInfoManager.getLastCheckTimeMillis();
long dataAge = System.currentTimeMillis() - lastCheckMillis;
return dataAge > TimeUnit.HOURS.toMillis(4);
}
}
Etapa 4: limpar as atualizações aplicadas após a reinicialização do dispositivo
Embora o UpdateInfoManager remova automaticamente as atualizações obsoletas sempre que o
registerUpdate() é invocado, seu atualizador não vai chamar o
registerUpdate() novamente depois que uma atualização OTA terminar de ser instalada até o próximo
ciclo de sincronização programada do servidor. Para evitar que os apps clientes vejam uma
atualização já instalada como ainda pendente imediatamente após a reinicialização, detecte
ACTION_BOOT_COMPLETED e chame
UpdateInfoManager.unregisterUpdate() quando uma atualização OTA terminar de
ser instalada para limpar o registro do cache local. Fazer isso apenas quando uma
atualização terminar de ser instalada evita limpar incondicionalmente as atualizações pendentes
(desinstaladas) em todas as reinicializações normais do dispositivo. Como as chaves UpdateInfoManager atualizam registros por componente, basta especificar o componente de destino ao construir o objeto UpdateInfo para cancelamento do registro:
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());
Etapa 5: verificar a integração
Execute as seguintes verificações em um dispositivo Android ou emulador usando o Android Debug Bridge (ADB) para validar a integração de ponta a ponta e evitar problemas comuns de implantação de OEM:
Verifique se os clientes confiam no seu provedor:os apps clientes ignoram qualquer provedor que não tenha
READ_PRIVILEGED_PHONE_STATE, mesmo que ele esteja pré-instalado. Confirme se a permissão foi concedida:adb shell dumpsys package <your_package_name> | grep "READ_PRIVILEGED_PHONE_STATE: granted=true"Em seguida, confirme se o serviço pode ser descoberto e não tem permissão. Na saída do seu serviço, verifique
exported=trueepermission=null:adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICESe um cliente ainda não encontrar seu provedor, verifique o logcat para
Ignoring untrusted update providerda tagSecurityPatchState.Verifique a resolução de intents no Usuário 0 e nos perfis de trabalho:confirme se o SO Android
PackageManagerresolve o filtro de intentUPDATE_INFO_SERVICEexportado no usuário principal (User 0) e em qualquer perfil de trabalho do Android Enterprise ativo (comoUser 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_SERVICEVerifique o estado do serviço e os registros em cache usando
dumpsys:UpdateInfoServicesubstituidump()para informarGlobal Last Check,Should Throttle(o estado do limitador de taxa) eCached Updates. Como oUpdateInfoServiceé um serviço vinculado e os clientes são desvinculados imediatamente após a consulta, odumpsys activity servicegera(nothing)quando nenhum cliente está vinculado. Inicie o serviço explicitamente antes de executardumpsys: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>Exemplo de saída de diagnóstico:
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 2026Acione a vinculação do cliente e verifique os resultados da telemetria:em um app de teste sem privilégios (sem permissões de assinatura do sistema), invoque
SecurityPatchState.queryAllAvailableUpdates(). Se você implementou os callbacks de telemetria, verifique o seguinte:- Verifique se o cliente sem privilégios faz vinculação sem um
SecurityExceptione acionaonClientConnected(packageName, callerUid). - Para provedores de modelo push (
shouldFetchUpdates() == false): verifique se os registros deonRequestCompleted(telemetry)UpdateFetchOutcome.CACHE_HIT(1) comfetchDurationMillis == 0em todas as consultas. - Para provedores de modelo de pull (
shouldFetchUpdates() == true): verifique seonRequestCompleted(telemetry)registraUpdateFetchOutcome.FETCHED(3) na consulta inicial de cache desatualizado, seguida porCACHE_HIT(1) em consultas subsequentes imediatas. Para redefinir o limitador de taxa persistente de uma hora entre as execuções de teste do modelo de pull, executeadb shell pm clear <your_package_name>.
- Verifique se o cliente sem privilégios faz vinculação sem um
Configurações opcionais e avançadas
Política de cache e limitação de taxa
Quando um cliente consulta atualizações, o UpdateInfoService executa um
fluxo de trabalho de bloqueio de verificação dupla para equilibrar a atualização de dados com a carga do servidor de back-end:

- Caminho rápido (
shouldFetchUpdates()): por padrão, oshouldFetchUpdates()retornatrue(indicando um cache desatualizado) somente quando olastCheckTimeMillisglobal tem mais de uma hora (TimeUnit.HOURS.toMillis(1)). QuandoshouldFetchUpdates()retornafalse, o serviço retorna imediatamente registros em cache com o resultadoUpdateFetchOutcome.CACHE_HITsem adquirir bloqueios ou realizar E/S de rede. É possível substituirshouldFetchUpdates()para personalizar essa política de armazenamento em cache. - Caminho lento e coalescência de solicitações:quando
shouldFetchUpdates()retornatrue, o serviço adquire um mutex de corrotina interna e reavaliashouldFetchUpdates()(retornandoUpdateFetchOutcome.COALESCEDse uma solicitação simultânea já tiver atualizado o cache enquanto aguardava o bloqueio). - Limitador de taxa persistente (
shouldThrottle()): para proteger a infraestrutura de back-end contra bursts de consultas ou falhas repetidas, oshouldThrottle()impõe um intervalo mínimo persistente de uma hora em reinicializações de apps e dispositivos.UpdateInfoServiceregistra cada tentativa antes de invocarfetchUpdates(). Portanto, sefetchUpdates()gerar uma exceção (retornandoUpdateFetchOutcome.FAILEDapós invocaronFetchFailed(e)), as consultas subsequentes durante os próximos 60 minutos retornarão dados de substituição em cache com o resultadoUpdateFetchOutcome.THROTTLED.
Observabilidade, telemetria e diagnósticos
O UpdateInfoService fornece hooks de observabilidade integrados para rastrear a adoção do cliente, monitorar a latência de IPC e registrar erros de back-end sem instrumentar stubs AIDL de baixo nível:
onRequestCompleted(telemetry):invocado após a conclusão de cada verificação de atualização com um resumoUpdateCheckTelemetry.onClientConnected(packageName, callerUid):é invocado quando um cliente verificado abre uma sessão.onClientDisconnected(packageName, callerUid):invocado quando um cliente desvincula ou o processo dele é encerrado.onFetchFailed(e):invocado se uma exceção ocorrer durantefetchUpdates(), antes que o serviço retorne dados de substituição armazenados em cache.
Substitua esses callbacks em UpdateInfoService (Kotlin) ou
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);
}
}
Resultados de telemetria e métricas de latência
UpdateCheckTelemetry mede durações decorridas monotônicas (SystemClock.elapsedRealtime()) e informa um de cinco resultados definidos em UpdateFetchOutcome:
| Constante de resultado | @IntDef Código |
Propriedades de métricas registradas | Descrição e estado do sistema |
|---|---|---|---|
UpdateFetchOutcome.CACHE_HIT |
1 |
totalDurationMillis, processingDurationMillis, callerUid |
Servido imediatamente do cache de disco/cache em memória local no Fast Path (shouldFetchUpdates() retornou false). lockWaitDurationMillis e fetchDurationMillis são 0. |
UpdateFetchOutcome.COALESCED |
2 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
Consulta enfileirada atrás de outra atualização ativa. Ao adquirir o bloqueio, os dados estavam atualizados. Evitou busca de rede duplicada (fetchDurationMillis é 0). |
UpdateFetchOutcome.FETCHED |
3 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid |
A sincronização da rede de back-end foi executada (fetchUpdates() concluído). Novos registros persistidos no disco. |
UpdateFetchOutcome.THROTTLED |
4 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
Solicitação bloqueada pelo limitador de taxa (shouldThrottle() retornou true). Os dados em cache foram retornados com segurança ao cliente (fetchDurationMillis é 0). |
UpdateFetchOutcome.FAILED |
5 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid |
A verificação de atualização ou a solicitação de rede gerou uma exceção. Capturado pelo firewall de exceção, disparou onFetchFailed(e), retornou o substituto em cache. |
Hook avançado do agente de serviço: getCallerUid()
Quando um cliente se conecta, UpdateInfoService avalia automaticamente
getCallerUid() na linha de execução inicial do Binder (antes de chamar
Binder.clearCallingIdentity() antes de fetchUpdates()), verifica
a propriedade do pacote e transmite o UID do caller verificado diretamente para
onClientConnected(), onClientDisconnected() e telemetry.callerUid em
onRequestCompleted(telemetry).
Para componentes padrão do Android <service>, não é necessário chamar nem substituir
getCallerUid(). O método protected open getCallerUid() (que delega a Binder.getCallingUid() por padrão) é fornecido como um hook de substituição para apps host que roteiam o IPC do Binder por um agente de serviço interno ou uma arquitetura de proxy, permitindo que a subclasse retorne o UID do cliente lógico em vez do UID do agente.
Outros recursos
Para mais informações sobre a publicação do estado de segurança, consulte os seguintes recursos:
Documentação
- Entender o estado de segurança do dispositivo
- Boletins de segurança do Android
- Componentes modulares do sistema
- Patches de segurança complementares
- Notas da versão do Security State Provider 1.0.0
Referência da API
UpdateInfoServiceListenableFutureUpdateInfoServiceUpdateInfoManagerUpdateInfoUpdateInfo.BuilderUpdateCheckTelemetryUpdateFetchOutcomeSecurityPatchState