Publicar o estado de segurança do dispositivo

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:

A biblioteca AndroidX Security State Provider estabelece uma estrutura de IPC padronizada e segura entre apps clientes 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 para android: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:

  1. 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=true e permission=null:

    adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICE
    

    Se um cliente ainda não encontrar seu provedor, verifique o logcat para Ignoring untrusted update provider da tag SecurityPatchState.

  2. Verifique a resolução de intents no Usuário 0 e nos perfis de trabalho:confirme se o SO Android PackageManager resolve o filtro de intent UPDATE_INFO_SERVICE exportado no usuário principal (User 0) e em qualquer perfil de trabalho do Android Enterprise ativo (como 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. Verifique o estado do serviço e os registros em cache usando dumpsys: UpdateInfoService substitui dump() para informar Global Last Check, Should Throttle (o estado do limitador de taxa) e Cached Updates. Como o UpdateInfoService é um serviço vinculado e os clientes são desvinculados imediatamente após a consulta, o dumpsys activity service gera (nothing) quando nenhum cliente está vinculado. Inicie o serviço explicitamente antes de executar 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>
    

    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 2026
    
  4. Acione 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 SecurityException e aciona onClientConnected(packageName, callerUid).
    • Para provedores de modelo push (shouldFetchUpdates() == false): verifique se os registros de onRequestCompleted(telemetry) UpdateFetchOutcome.CACHE_HIT (1) com fetchDurationMillis == 0 em todas as consultas.
    • Para provedores de modelo de pull (shouldFetchUpdates() == true): verifique se onRequestCompleted(telemetry) registra UpdateFetchOutcome.FETCHED (3) na consulta inicial de cache desatualizado, seguida por CACHE_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, execute adb shell pm clear <your_package_name>.

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:

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, o shouldFetchUpdates() retorna true (indicando um cache desatualizado) somente quando o lastCheckTimeMillis global tem mais de uma hora (TimeUnit.HOURS.toMillis(1)). Quando shouldFetchUpdates() retorna false, o serviço retorna imediatamente registros em cache com o resultado UpdateFetchOutcome.CACHE_HIT sem adquirir bloqueios ou realizar E/S de rede. É possível substituir shouldFetchUpdates() para personalizar essa política de armazenamento em cache.
  • Caminho lento e coalescência de solicitações:quando shouldFetchUpdates() retorna true, o serviço adquire um mutex de corrotina interna e reavalia shouldFetchUpdates() (retornando UpdateFetchOutcome.COALESCED se 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, o shouldThrottle() impõe um intervalo mínimo persistente de uma hora em reinicializações de apps e dispositivos. UpdateInfoService registra cada tentativa antes de invocar fetchUpdates(). Portanto, se fetchUpdates() gerar uma exceção (retornando UpdateFetchOutcome.FAILED após invocar onFetchFailed(e)), as consultas subsequentes durante os próximos 60 minutos retornarão dados de substituição em cache com o resultado UpdateFetchOutcome.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:

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

Referência da API