Si vous êtes un fabricant d'équipement d'origine (OEM) ou que vous gérez un client de mise à jour OTA (Over-The-Air) privilégié, vous pouvez donner aux applications sensibles à la sécurité sur l'appareil une visibilité sur les mises à jour de sécurité en attente afin qu'elles puissent évaluer précisément la posture de sécurité de l'appareil. Pour appliquer des règles strictes de type "zero trust", les applications doivent pouvoir vérifier non seulement le niveau du correctif installé sur l'appareil (niveau du correctif de sécurité de l'appareil ou DSPL), mais aussi les mises à jour de sécurité disponibles et prêtes à être installées (niveau du correctif de sécurité disponible ou ASPL).
Étant donné que les applications clientes non privilégiées ne peuvent pas lire directement les propriétés du micrologiciel, inspecter les bases de données privées du programme de mise à jour ni interroger les points de terminaison internes du backend OEM, la bibliothèque AndroidX Security State Provider fournit une architecture de communication interprocessus (IPC) standardisée et sécurisée que les clients de mise à jour utilisent pour partager des informations sur les mises à jour disponibles. En implémentant un UpdateInfoService dans votre client de mise à jour OTA, vous pouvez publier des métadonnées ASPL pour le système sans exposer les intégrations de backend propriétaires. Bien que Google fournisse une implémentation pour les mises à jour des composants système modulaires (Mainline) pour les appareils GMS, les appareils non GMS peuvent également publier des métadonnées ASPL pour ces composants système modulaires.
Présentation de l'architecture
Le schéma suivant illustre la façon dont la bibliothèque AndroidX Security State Provider établit un framework IPC sécurisé et standardisé entre les applications clientes non privilégiées et les services de mise à jour sur l'appareil :

Modèles de diffusion des données
Les applications clientes interrogent la disponibilité des mises à jour en appelant queryAllAvailableUpdates ou fetchAvailableSecurityPatchLevel. En arrière-plan, la bibliothèque cliente détecte automatiquement tous les services enregistrés qui étendent la classe UpdateInfoService sur l'appareil à partir des applications système qui détiennent l'autorisation READ_PRIVILEGED_PHONE_STATE et s'y lie.
Comme illustré dans le schéma précédent, la bibliothèque security-state-provider est compatible avec deux modèles de diffusion de données :
| Modèle de diffusion | Déclencheur de synchronisation | Réponse du client | Cas d'utilisation recommandés |
|---|---|---|---|
| Modèle push (synchronisation en arrière-plan) | Les nœuds de calcul en arrière-plan planifiés (WorkManager ou JobScheduler) se synchronisent avec votre backend et écrivent des enregistrements dans UpdateInfoManager. Votre service diffuse toujours le contenu à partir du cache du disque local (shouldFetchUpdates() = false). |
Elles sont diffusées immédiatement à partir du cache local. | Les outils de mise à jour OTA du système OEM et les outils de mise à jour des composants modulaires synchronisés en arrière-plan. |
| Modèle pull (synchronisation à la demande) | Les requêtes IPC client entrantes déclenchent une récupération réseau lorsque les enregistrements mis en cache sont obsolètes (shouldFetchUpdates() = true). La coalescence des mutex et la limitation du débit (shouldThrottle()) protègent votre backend contre les pics. |
Attend la récupération du backend lorsque le cache est obsolète. | Mises à jour OTA OEM monolithiques sans planificateurs de synchronisation en arrière-plan. |
Plusieurs fournisseurs de mises à jour
Sur les appareils Android de production, plusieurs fournisseurs de mises à jour indépendants coexistent simultanément. Par exemple, Mainline publie la disponibilité des composants modulaires (COMPONENT_SYSTEM_MODULES), tandis que votre client OTA OEM publie les mises à jour de l'image OS principale (COMPONENT_SYSTEM).
Votre service n'a besoin d'enregistrer les mises à jour que pour les composants spécifiques qu'il gère. Si plusieurs fournisseurs sur un appareil publient des mises à jour pour le même composant, les applications clientes évaluent le niveau de correctif le plus élevé disponible (à l'aide de fetchAvailableSecurityPatchLevel()) ou inspectent les enregistrements UpdateInfo individuels (à l'aide de queryAllAvailableUpdates()) pour l'audit d'entreprise. Assurez-vous que votre service publie toujours le format canonique de votre composant (DateBasedSecurityPatchLevel pour COMPONENT_SYSTEM).
Guide par étapes pour intégrer votre client de mise à jour
Suivez ces étapes pour intégrer la bibliothèque AndroidX Security State Provider à votre client de mise à jour et commencer à publier la disponibilité des mises à jour de sécurité de votre appareil.
Étape 1 : ajoutez des dépendances
Pour implémenter un fournisseur de mise à jour, assurez-vous que votre projet inclut le dépôt Maven de Google, puis ajoutez la bibliothèque security-state-provider au fichier build.gradle.kts (DSL Kotlin) ou build.gradle (DSL Groovy) de votre module :
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'
}
Étape 2 : Déclarez le service de mise à jour dans votre fichier manifeste
Déclarez votre service dans le AndroidManifest.xml de votre application avec un <intent-filter> correspondant à androidx.security.state.provider.UPDATE_INFO_SERVICE. Le service doit être exporté (android:exported="true") et configuré en tant que service mono-utilisateur (android:singleUser="true") afin que la bibliothèque cliente puisse s'y lier au-delà des limites de processus et d'utilisateur, en particulier pour les profils professionnels :
<!-- 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>
Si votre programme de mise à jour ne s'exécute pas en tant que android.uid.system, déclarez également les autorisations suivantes dans votre fichier manifeste et assurez-vous de les ajouter à votre liste d'autorisation des autorisations privilégiées :
READ_PRIVILEGED_PHONE_STATE: obligatoire pour que les clients fassent confiance à votre fournisseur.INTERACT_ACROSS_USERS: obligatoire pourandroid:singleUser="true".
Étape 3 : Implémenter UpdateInfoService
Pour publier l'état de votre mise à jour, vous devez implémenter la classe UpdateInfoService et créer des enregistrements de mise à jour qui correspondent au modèle de données attendu.
Spécification du modèle de données UpdateInfo
Que vous choisissiez le modèle Push ou Pull, construisez des enregistrements UpdateInfo à l'aide de UpdateInfo.Builder en suivant les spécifications suivantes :
| Field Name (Nom du champ) | Méthode Getter | Type de données | Exigences concernant la validation et le format | Finalité et sémantique du système |
|---|---|---|---|---|
component |
getComponent() |
String (@Component) |
Constantes canoniques dans SecurityPatchState : COMPONENT_SYSTEM, COMPONENT_SYSTEM_MODULES ou COMPONENT_KERNEL. |
Identifie le sous-système logiciel ou micrologiciel ciblé par cette mise à jour. |
securityPatchLevel |
getSecurityPatchLevel() |
SecurityPatchLevel |
Doit être une instance de DateBasedSecurityPatchLevel (YYYY-MM-DD) ou VersionedSecurityPatchLevel (major.minor.patch), ou être analysé à l'aide de SecurityPatchState.getComponentSecurityPatchLevel(). |
Niveau du correctif de sécurité cible qui sera atteint une fois cette mise à jour installée. |
publishedDateMillis |
getPublishedDateMillis() |
long |
Millisecondes depuis l'epoch Unix (System.currentTimeMillis()). Doit être > 0. |
Date à laquelle la mise à jour a été mise à la disposition des utilisateurs, par exemple l'heure de publication OTA. N'utilisez pas le temps de téléchargement ni d'installation de la charge utile. |
lastCheckTimeMillis |
getLastCheckTimeMillis() |
long |
Millisecondes depuis l'epoch Unix. doit être > 0 |
Code temporel indiquant quand votre fournisseur a vérifié ou découvert cet enregistrement de mise à jour lors de la synchronisation. |
Choisissez le modèle de diffusion qui correspond à l'architecture de votre programme de mise à jour parmi les options suivantes :
Option A : Modèle Push (recommandé)
Lorsque votre worker de synchronisation en arrière-plan vérifie votre serveur OTA, validez toute mise à jour découverte qui fait progresser le niveau de correctif actuel de l'appareil et conservez-le à l'aide de UpdateInfoManager.registerUpdate(), ou appelez UpdateInfoManager.unregisterUpdate() si aucune mise à jour de sécurité avancée n'est en attente. Appelez toujours UpdateInfoManager.setLastCheckTimeMillis() à la fin de chaque synchronisation (même après avoir appelé registerUpdate(), qui conserve l'enregistrement UpdateInfo par composant, mais ne met pas à jour le code temporel de la dernière vérification globale renvoyé aux clients). Vous pouvez implémenter cela avec un WorkManager CoroutineWorker en Kotlin ou un Worker en 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();
}
}
Dans un modèle basé sur le push, les tâches en arrière-plan conservent les enregistrements de mise à jour directement dans UpdateInfoManager. Pour indiquer au framework de toujours diffuser les enregistrements à partir du stockage sur disque local, remplacez shouldFetchUpdates() pour renvoyer false en étendant UpdateInfoService en Kotlin ou ListenableFutureUpdateInfoService en 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());
}
}
Option B : Modèle pull (à la demande)
Dans une architecture basée sur l'extraction, votre service gère les demandes d'actualisation à la demande déclenchées par les applications clientes lorsque le cache local est obsolète.
Pour gérer les requêtes de mise à jour à la demande, étendez UpdateInfoService en Kotlin (en implémentant la fonction de suspension fetchUpdates()) ou ListenableFutureUpdateInfoService en Java (en implémentant fetchUpdatesAsync() renvoyant un ListenableFuture 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);
}
}
Étape 4 : Supprimez les mises à jour appliquées après le redémarrage de l'appareil
Bien que UpdateInfoManager supprime automatiquement les mises à jour obsolètes chaque fois que registerUpdate() est appelé, votre programme de mise à jour n'appellera plus registerUpdate() une fois l'installation d'une mise à jour OTA terminée, jusqu'au prochain cycle de synchronisation du serveur planifié. Pour empêcher les applications clientes de considérer une mise à jour déjà installée comme toujours en attente immédiatement après le redémarrage, écoutez ACTION_BOOT_COMPLETED et appelez UpdateInfoManager.unregisterUpdate() lorsqu'une mise à jour OTA a fini de s'installer pour effacer l'enregistrement du cache local. Cela permet d'éviter d'effacer inconditionnellement les mises à jour en attente (désinstallées) à chaque redémarrage normal de l'appareil. Étant donné que les clés UpdateInfoManager mettent à jour les enregistrements par composant, il vous suffit de spécifier le composant cible lors de la construction de l'objet UpdateInfo pour la désinscription :
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());
Étape 5 : Vérifiez votre intégration
Exécutez les vérifications suivantes sur un appareil ou un émulateur Android à l'aide d'Android Debug Bridge (ADB) pour valider l'intégration de bout en bout et éviter les pièges courants liés au déploiement OEM :
Vérifiez que les clients font confiance à votre fournisseur : les applications clientes ignorent tout fournisseur qui ne détient pas
READ_PRIVILEGED_PHONE_STATE, même s'il est préinstallé. Vérifiez que l'autorisation est accordée :adb shell dumpsys package <your_package_name> | grep "READ_PRIVILEGED_PHONE_STATE: granted=true"Vérifiez ensuite que votre service est détectable et qu'il ne dispose d'aucune autorisation de service. Dans le résultat de votre service, recherchez
exported=trueetpermission=null:adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICESi un client ne voit toujours pas votre fournisseur, vérifiez si
Ignoring untrusted update providerest présent dans logcat à partir de la baliseSecurityPatchState.Vérifiez la résolution de l'intent dans le profil utilisateur 0 et le profil professionnel : assurez-vous que l'
PackageManagerdu système d'exploitation Android résout votre filtre d'intentUPDATE_INFO_SERVICEexporté à la fois dans l'utilisateur principal (User 0) et dans tout profil professionnel Android Enterprise actif (tel queUser 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_SERVICEVérifiez l'état du service et les enregistrements mis en cache à l'aide de
dumpsys:UpdateInfoServiceremplacedump()pour signalerGlobal Last Check,Should Throttle(l'état du limiteur de fréquence) etCached Updates. Étant donné queUpdateInfoServiceest un service lié et que les clients se dissocient immédiatement après l'interrogation,dumpsys activity servicegénère(nothing)lorsqu'aucun client n'est lié. Démarrez explicitement le service avant d'exécuterdumpsys: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>Exemple de résultat de diagnostic :
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 2026Déclenchez l'association du client et vérifiez les résultats de la télémétrie : à partir d'une application de test non privilégiée (ne disposant pas des autorisations de signature système), appelez
SecurityPatchState.queryAllAvailableUpdates(). Si vous avez implémenté les rappels de télémétrie, vérifiez les points suivants :- Vérifiez que le client non privilégié se lie sans
SecurityExceptionet déclencheonClientConnected(packageName, callerUid). - Pour les fournisseurs de modèle Push (
shouldFetchUpdates() == false) : vérifiez que les journauxonRequestCompleted(telemetry)UpdateFetchOutcome.CACHE_HIT(1) avecfetchDurationMillis == 0sont présents pour chaque requête. - Pour les fournisseurs de modèle Pull (
shouldFetchUpdates() == true) : vérifiez que les journauxonRequestCompleted(telemetry)UpdateFetchOutcome.FETCHED(3) s'affichent sur la requête initiale de cache obsolète, suivis deCACHE_HIT(1) sur les requêtes suivantes immédiates. (Pour réinitialiser le limiteur de fréquence persistant d'une heure entre les exécutions de tests du modèle Pull, exécutezadb shell pm clear <your_package_name>.)
- Vérifiez que le client non privilégié se lie sans
Configurations facultatives et avancées
Règles de mise en cache et limitation du débit
Lorsqu'un client interroge les mises à jour, UpdateInfoService exécute un workflow de verrouillage à double vérification pour équilibrer la fraîcheur des données par rapport à la charge du serveur de backend :

- Chemin rapide (
shouldFetchUpdates()) : par défaut,shouldFetchUpdates()renvoietrue(indiquant un cache obsolète) uniquement lorsque lelastCheckTimeMillisglobal a plus d'une heure (TimeUnit.HOURS.toMillis(1)). LorsqueshouldFetchUpdates()renvoiefalse, le service renvoie immédiatement les enregistrements mis en cache avec le résultatUpdateFetchOutcome.CACHE_HITsans acquérir de verrous ni effectuer d'E/S réseau. Vous pouvez remplacershouldFetchUpdates()pour personnaliser cette stratégie de mise en cache. - Chemin lent et fusion des requêtes : lorsque
shouldFetchUpdates()renvoietrue, le service acquiert un mutex de coroutine interne et réévalueshouldFetchUpdates()(en renvoyantUpdateFetchOutcome.COALESCEDsi une requête simultanée a déjà actualisé le cache en attendant le verrou). - Limiteur de débit persistant (
shouldThrottle()) : pour protéger l'infrastructure de backend contre les pics de requêtes ou les échecs répétés,shouldThrottle()applique un intervalle persistant d'une heure minimum lors des redémarrages d'applications et d'appareils.UpdateInfoServiceenregistre chaque tentative avant d'appelerfetchUpdates(). Par conséquent, sifetchUpdates()génère une exception (en renvoyantUpdateFetchOutcome.FAILEDaprès avoir appeléonFetchFailed(e)), les requêtes suivantes au cours des 60 minutes suivantes renvoient gracieusement des données de secours mises en cache avec le résultatUpdateFetchOutcome.THROTTLED.
Observabilité, télémétrie et diagnostics
UpdateInfoService fournit des hooks d'observabilité intégrés pour suivre l'adoption par les clients, surveiller la latence IPC et enregistrer les erreurs de backend sans instrumenter les stubs AIDL de bas niveau :
onRequestCompleted(telemetry): appelé à la fin de chaque vérification de mise à jour avec un récapitulatifUpdateCheckTelemetry.onClientConnected(packageName, callerUid): appelé lorsqu'un client validé ouvre une session.onClientDisconnected(packageName, callerUid): appelé lorsqu'un client se désinscrit ou que son processus se termine.onFetchFailed(e): appelé si une exception se produit pendantfetchUpdates(), avant que le service ne renvoie les données de secours mises en cache.
Remplacez ces rappels dans 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);
}
}
Résultats de la télémétrie et métriques de latence
UpdateCheckTelemetry mesure les durées écoulées monotones (SystemClock.elapsedRealtime()) et renvoie l'un des cinq résultats définis dans UpdateFetchOutcome :
| Constante de résultat | @IntDef Code |
Propriétés des métriques enregistrées | Description et état du système |
|---|---|---|---|
UpdateFetchOutcome.CACHE_HIT |
1 |
totalDurationMillis, processingDurationMillis, callerUid |
Diffusé immédiatement à partir du cache de disque/mémoire local sur le chemin rapide (shouldFetchUpdates() renvoyé false). lockWaitDurationMillis et fetchDurationMillis sont 0. |
UpdateFetchOutcome.COALESCED |
2 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
Requête mise en file d'attente derrière une autre actualisation active ; les données étaient à jour lors de l'acquisition du verrou. Récupération de réseau en double évitée (fetchDurationMillis est 0). |
UpdateFetchOutcome.FETCHED |
3 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis et callerUid |
La synchronisation du réseau backend a bien été effectuée (fetchUpdates() terminé). Nouveaux enregistrements conservés sur le disque. |
UpdateFetchOutcome.THROTTLED |
4 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid |
Requête bloquée par le limiteur de débit (shouldThrottle() renvoie true). Les données mises en cache sont renvoyées de manière sécurisée au client (fetchDurationMillis est 0). |
UpdateFetchOutcome.FAILED |
5 |
totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis et callerUid |
Une exception a été générée lors de la vérification des mises à jour ou d'une requête réseau. Caught by exception firewall, fired onFetchFailed(e), returned cached fallback. |
Hook d'agent de service avancé : getCallerUid()
Lorsqu'un client se connecte, UpdateInfoService évalue automatiquement getCallerUid() sur le thread Binder initial (avant d'appeler Binder.clearCallingIdentity() avant fetchUpdates()), vérifie la propriété du package et transmet l'UID de l'appelant validé directement à onClientConnected(), onClientDisconnected() et telemetry.callerUid dans onRequestCompleted(telemetry).
Pour les composants <service> Android standards, vous n'avez pas besoin d'appeler ni de remplacer getCallerUid(). La méthode protected open getCallerUid() (qui délègue à Binder.getCallingUid() par défaut) est fournie en tant que crochet de remplacement pour les applications hôtes qui acheminent Binder IPC via un agent de service interne ou une architecture de proxy, ce qui permet à la sous-classe de renvoyer l'UID du client logique plutôt que l'UID de l'agent.
Ressources supplémentaires
Pour en savoir plus sur la publication de l'état de sécurité, consultez les ressources suivantes :
Documentation
- Comprendre l'état de sécurité de l'appareil
- Bulletins de sécurité Android
- Composants du système modulaire
- Correctifs de sécurité supplémentaires
- Notes de version du fournisseur d'état de sécurité 1.0.0
Documentation de référence de l'API
UpdateInfoServiceListenableFutureUpdateInfoServiceUpdateInfoManagerUpdateInfoUpdateInfo.BuilderUpdateCheckTelemetryUpdateFetchOutcomeSecurityPatchState