デバイスのセキュリティ状態を公開する

相手先ブランド製造(OEM)または特権の無線(OTA)アップデート クライアントを維持している場合は、デバイス上のセキュリティに敏感なアプリに保留中のセキュリティ アップデートを認識させ、デバイスのセキュリティ ポスチャーを正確に評価できるようにすることができます。堅牢なゼロトラスト ポリシーを適用するには、アプリはデバイスにインストールされているパッチレベル(デバイス セキュリティ パッチレベル、DSPL)だけでなく、利用可能でインストール可能なセキュリティ アップデート(利用可能なセキュリティ パッチレベル、ASPL)も確認できる必要があります。

権限のないクライアント アプリはファームウェア プロパティを直接読み取ったり、プライベート アップデータ データベースを検査したり、内部 OEM バックエンド エンドポイントをクエリしたりできないため、AndroidX Security State Provider ライブラリは、アップデート クライアントが利用可能なアップデートに関する情報を共有するために使用する、標準化された安全なプロセス間通信(IPC)アーキテクチャを提供します。OTA アップデート クライアントに UpdateInfoService を実装することで、独自のバックエンド統合を公開することなく、システムの ASPL メタデータを公開できます。Google は GMS デバイス向けにモジュラー システム コンポーネント(Mainline)アップデータの実装を提供しますが、GMS 以外のデバイスでもこれらのモジュラー システム コンポーネントの ASPL メタデータを公開できます。

アーキテクチャの概要

次の図は、AndroidX Security State Provider ライブラリが、権限のないクライアント アプリとデバイス上のアップデート サービスとの間に、標準化された安全な IPC フレームワークを確立する仕組みを示しています。

AndroidX Security State Provider ライブラリは、権限のないクライアント アプリとオンデバイス アップデート サービスの間で標準化された安全な IPC フレームワークを確立します。

データ配信モデル

クライアント アプリは、queryAllAvailableUpdates または fetchAvailableSecurityPatchLevel を呼び出して、アップデートの利用可能性をクエリします。内部的には、クライアント ライブラリが、READ_PRIVILEGED_PHONE_STATE 権限を持つシステムアプリから、デバイス上の UpdateInfoService クラスを拡張するすべての登録済みサービスを自動的に検出してバインドします。

上の図に示すように、security-state-provider ライブラリは次の 2 つのデータ配信モデルをサポートしています。

実装モデル 同期トリガー クライアント レスポンス おすすめのユースケース
プッシュモデル(バックグラウンド同期) スケジュール設定されたバックグラウンド ワーカー(WorkManager または JobScheduler)は、バックエンドと同期して、UpdateInfoManager にレコードを書き込みます。サービスは常にローカル ディスク キャッシュ(shouldFetchUpdates() = false)から提供されます。 ローカル キャッシュからすぐに提供されます。 OEM システム OTA アップデータと、バックグラウンドで同期されるモジュラー コンポーネント アップデータ。
pull モデル(オンデマンド同期) キャッシュに保存されたレコードが古い場合(shouldFetchUpdates() = true)、受信クライアント IPC クエリによってネットワーク フェッチがトリガーされます。Mutex の統合とレート制限(shouldThrottle())により、バックエンドがスパイクから保護されます。 キャッシュが古い場合にバックエンド フェッチを待機します。 スケジュール設定されたバックグラウンド同期ワーカーのないモノリシック OEM OTA アップデータ。

複数の更新プロバイダ

本番環境の Android デバイスでは、複数の独立した更新プロバイダが同時に共存します。たとえば、Mainline はモジュラー コンポーネント(COMPONENT_SYSTEM_MODULES)の可用性を公開し、OEM OTA クライアントはプライマリ OS イメージ(COMPONENT_SYSTEM)のアップデートを公開します。

サービスは、管理する特定のコンポーネントの更新のみを登録する必要があります。デバイス上の複数のプロバイダが同じコンポーネントのアップデートを公開する場合、クライアント アプリは、利用可能な最も高いパッチレベルを評価(fetchAvailableSecurityPatchLevel() を使用)するか、個々の UpdateInfo レコードを検査(queryAllAvailableUpdates() を使用)して、エンタープライズ監査を行います。サービスが常にコンポーネントの正規形式(COMPONENT_SYSTEM の場合は DateBasedSecurityPatchLevel)を公開するようにします。

更新クライアントをオンボーディングする手順ガイド

AndroidX Security State Provider ライブラリを更新クライアントに統合し、デバイスのセキュリティ アップデートの利用可能性の公開を開始する手順は次のとおりです。

ステップ 1: 依存関係を追加する

更新プロバイダを実装するには、プロジェクトに Google Maven リポジトリが含まれていることを確認してから、モジュールの build.gradle.kts(Kotlin DSL)または build.gradle(Groovy DSL)ファイルに security-state-provider ライブラリを追加します。

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'
}

ステップ 2: マニフェストで更新サービスを宣言する

androidx.security.state.provider.UPDATE_INFO_SERVICE に一致する <intent-filter> を使用して、アプリの AndroidManifest.xml でサービスを宣言します。サービスは、プロセスとユーザーの境界(特に仕事用プロファイル)を越えてクライアント ライブラリがバインドできるように、エクスポート(android:exported="true")され、シングル ユーザー サービス(android:singleUser="true")として構成されている必要があります。

<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.android.updater">
    <application>
        <service
            android:name=".MyUpdateInfoService"
            android:exported="true"
            android:singleUser="true"
            tools:ignore="ExportedService">
            <intent-filter>
                <action android:name="androidx.security.state.provider.UPDATE_INFO_SERVICE" />
            </intent-filter>
        </service>
    </application>
</manifest>

アップデータが android.uid.system として実行されない場合は、マニフェストで次の権限も宣言し、特権権限の許可リストに追加してください。

  • READ_PRIVILEGED_PHONE_STATE: クライアントがプロバイダを信頼するために必要です。
  • INTERACT_ACROSS_USERS: android:singleUser="true" で必須。

ステップ 3: UpdateInfoService を実装する

更新ステータスを公開するには、UpdateInfoService クラスを実装し、想定されるデータモデルに一致する更新レコードを構築する必要があります。

UpdateInfo データモデルの仕様

プッシュモデルとプルモデルのどちらを選択する場合でも、次の仕様に従って UpdateInfo.Builder を使用して UpdateInfo レコードを構築します。

フィールド名 Getter メソッド データ型 検証と形式の要件 目的とシステム セマンティクス
component getComponent() String(@Component) SecurityPatchState の正規定数: COMPONENT_SYSTEM、COMPONENT_SYSTEM_MODULES、COMPONENT_KERNEL。 このアップデートの対象となるソフトウェアまたはファームウェアのサブシステムを特定します。
securityPatchLevel getSecurityPatchLevel() SecurityPatchLevel DateBasedSecurityPatchLevel(YYYY-MM-DD)または VersionedSecurityPatchLevel(major.minor.patch)のインスタンスであるか、SecurityPatchState.getComponentSecurityPatchLevel() を使用して解析する必要があります。 このアップデートがインストールされた後に達成されるターゲット セキュリティ パッチレベル。
publishedDateMillis getPublishedDateMillis() long Unix エポックからのミリ秒数(System.currentTimeMillis())。> 0 である必要があります。 アップデートがユーザーに提供された日時(OTA リリース時間など)。ペイロードのダウンロード時間やインストール時間は使用しないでください。
lastCheckTimeMillis getLastCheckTimeMillis() long Unix エポックからのミリ秒数。> 0 を指定します。 同期中にプロバイダがこの更新レコードを確認または検出したときのタイムスタンプ。

次のオプションから、アップデータ アーキテクチャに合った配信モデルを選択します。

オプション A: プッシュモデル(推奨)

バックグラウンド同期ワーカーが OTA サーバーをチェックする際、検出されたアップデートがデバイスの現在のパッチレベルを進めることを検証し、UpdateInfoManager.registerUpdate() を使用して永続化するか、進むセキュリティ アップデートが保留中でない場合は UpdateInfoManager.unregisterUpdate() を呼び出します。同期の最後には必ず UpdateInfoManager.setLastCheckTimeMillis() を呼び出します(registerUpdate() を呼び出した後でも呼び出します。registerUpdate() はコンポーネントごとの UpdateInfo レコードを保持しますが、クライアントに返されるグローバルな最終チェックのタイムスタンプは更新しません)。これは、Kotlin の WorkManager CoroutineWorker または Java の Worker で実装できます。

Kotlin

import android.content.Context
import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlin.math.max

class OtaSyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        val updateInfoManager = UpdateInfoManager(applicationContext)
        val securityPatchState = SecurityPatchState(applicationContext)
        val currentSpl = securityPatchState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)

        // 1. Fetch available update metadata from OEM backend
        val latestUpdate = MyOtaClient.fetchLatestSystemUpdate()
        val targetSplString = latestUpdate?.spl?.trim()
        val targetSpl = if (!targetSplString.isNullOrEmpty()) {
            DateBasedSecurityPatchLevel.fromString(targetSplString)
        } else {
            null
        }

        // 2. Defensively verify that target SPL is non-blank AND strictly newer than installed DSPL.
        // If an update is a maintenance patch with no SPL increment (or if no update is available),
        // unregister any stale cached record for this component.
        if (latestUpdate != null && targetSpl != null && targetSpl > currentSpl) {
            val updateInfo = UpdateInfo.Builder()
                .setComponent(SecurityPatchState.COMPONENT_SYSTEM)
                .setSecurityPatchLevel(targetSpl)
                .setPublishedDateMillis(latestUpdate.releaseTimeMillis)
                .setLastCheckTimeMillis(System.currentTimeMillis())
                .build()
            updateInfoManager.registerUpdate(updateInfo)
        } else {
            val clearTarget = UpdateInfo.Builder()
                .setComponent(SecurityPatchState.COMPONENT_SYSTEM)
                .build()
            updateInfoManager.unregisterUpdate(clearTarget)
        }

        // 3. Update global freshness timestamp (monotonic synchronization)
        val currentCheckTime = System.currentTimeMillis()
        val previousCheckTime = updateInfoManager.getLastCheckTimeMillis()
        updateInfoManager.setLastCheckTimeMillis(max(previousCheckTime, currentCheckTime))
        return Result.success()
    }
}

Java

import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.UpdateInfoManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class OtaSyncWorker extends Worker {
    public OtaSyncWorker(@NonNull Context context, @NonNull WorkerParameters params) {
        super(context, params);
    }

    @NonNull
    @Override
    public Result doWork() {
        // In Java, pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
        UpdateInfoManager updateInfoManager =
                new UpdateInfoManager(getApplicationContext(), /* customSecurityState= */ null);
        SecurityPatchState securityPatchState = new SecurityPatchState(getApplicationContext());
        SecurityPatchLevel currentSpl =
                securityPatchState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);

        // 1. Fetch available update metadata from OEM backend
        MyOtaUpdate latestUpdate = MyOtaClient.fetchLatestSystemUpdate();
        String targetSplString = (latestUpdate != null && latestUpdate.getSpl() != null)
                ? latestUpdate.getSpl().trim()
                : null;
        DateBasedSecurityPatchLevel targetSpl =
                !TextUtils.isEmpty(targetSplString)
                        ? DateBasedSecurityPatchLevel.fromString(targetSplString)
                        : null;

        // 2. Defensively verify that target SPL is non-blank AND strictly newer than installed DSPL.
        // If an update is a maintenance patch with no SPL increment (or if no update is available),
        // unregister any stale cached record for this component.
        if (latestUpdate != null && targetSpl != null && targetSpl.compareTo(currentSpl) > 0) {
            UpdateInfo updateInfo = new UpdateInfo.Builder()
                    .setComponent(SecurityPatchState.COMPONENT_SYSTEM)
                    .setSecurityPatchLevel(targetSpl)
                    .setPublishedDateMillis(latestUpdate.getReleaseTimeMillis())
                    .setLastCheckTimeMillis(System.currentTimeMillis())
                    .build();
            updateInfoManager.registerUpdate(updateInfo);
        } else {
            UpdateInfo clearTarget = new UpdateInfo.Builder()
                    .setComponent(SecurityPatchState.COMPONENT_SYSTEM)
                    .build();
            updateInfoManager.unregisterUpdate(clearTarget);
        }

        // 3. Update global freshness timestamp (monotonic synchronization)
        long currentCheckTime = System.currentTimeMillis();
        long previousCheckTime = updateInfoManager.getLastCheckTimeMillis();
        updateInfoManager.setLastCheckTimeMillis(Math.max(previousCheckTime, currentCheckTime));
        return Result.success();
    }
}

プッシュベースのモデルでは、バックグラウンド タスクが更新レコードを UpdateInfoManager に直接保持します。フレームワークにローカル ディスク ストレージから常にレコードを配信するよう指示するには、Kotlin では UpdateInfoService を、Java では ListenableFutureUpdateInfoService を拡張して shouldFetchUpdates() をオーバーライドし、false を返します。

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());
    }
}

オプション B: プルモデル(オンデマンド)

プルベースのアーキテクチャでは、ローカル キャッシュが古い場合にクライアント アプリによってトリガーされるオンデマンド更新リクエストをサービスが処理します。

オンデマンド アップデートのクエリを処理するには、Kotlin で UpdateInfoService を拡張(suspend 関数 fetchUpdates() を実装)するか、Java で ListenableFutureUpdateInfoService を拡張(Guava の ListenableFuture を返す fetchUpdatesAsync() を実装)します。

Kotlin

package com.example.android.updater

import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.security.state.provider.UpdateInfoService
import java.util.concurrent.TimeUnit

class MyUpdateInfoService : UpdateInfoService() {
    // Manage local update records and check timestamps
    private val updateInfoManager by lazy { UpdateInfoManager(this) }

    override suspend fun fetchUpdates(): List<UpdateInfo> {
        val currentSpl = SecurityPatchState(this)
            .getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)

        // 1. Execute network request to OTA backend
        val response = MyOtaBackendClient.checkAvailableUpdates()

        // 2. Defensively filter out blank or non-advancing SPLs and map to UpdateInfo objects
        val validUpdates = response.updates
            .mapNotNull { updateItem ->
                val splString = updateItem.targetSpl?.trim()
                if (splString.isNullOrEmpty()) return@mapNotNull null
                val parsedSpl = DateBasedSecurityPatchLevel.fromString(splString)
                if (parsedSpl > currentSpl) {
                    UpdateInfo.Builder()
                        .setComponent(SecurityPatchState.COMPONENT_SYSTEM)
                        .setSecurityPatchLevel(parsedSpl)
                        .setPublishedDateMillis(updateItem.releaseTimestampMillis)
                        .setLastCheckTimeMillis(System.currentTimeMillis())
                        .build()
                } else {
                    null
                }
            }

        // 3. If no advancing SYSTEM update is available (or if a previously offered update was revoked),
        // proactively unregister any cached record for this component.
        if (validUpdates.isEmpty()) {
            val clearTarget = UpdateInfo.Builder()
                .setComponent(SecurityPatchState.COMPONENT_SYSTEM)
                .build()
            updateInfoManager.unregisterUpdate(clearTarget)
        }
        return validUpdates
    }

    override fun shouldFetchUpdates(): Boolean {
        // Enforce custom freshness threshold (for example, 4 hours instead of default 1 hour)
        val lastCheckMillis = updateInfoManager.getLastCheckTimeMillis()
        val dataAge = System.currentTimeMillis() - lastCheckMillis
        return dataAge > TimeUnit.HOURS.toMillis(4)
    }
}

Java

package com.example.android.updater;

import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import androidx.security.state.provider.UpdateInfoManager;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class MyUpdateInfoService extends ListenableFutureUpdateInfoService {
    private UpdateInfoManager updateInfoManager;

    @Override
    public void onCreate() {
        super.onCreate();
        // Pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
        updateInfoManager = new UpdateInfoManager(this, /* customSecurityState= */ null);
    }

    @NonNull
    @Override
    protected ListenableFuture<List<UpdateInfo>> fetchUpdatesAsync() {
        try {
            SecurityPatchLevel currentSpl = new SecurityPatchState(this)
                    .getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);
            MyOtaBackendResponse response = MyOtaBackendClient.checkAvailableUpdates();
            List<UpdateInfo> updates = new ArrayList<>();
            for (MyOtaUpdateItem item : response.getUpdates()) {
                String trimmedSpl = (item.getTargetSpl() != null) ? item.getTargetSpl().trim() : null;
                if (!TextUtils.isEmpty(trimmedSpl)) {
                    DateBasedSecurityPatchLevel parsedSpl =
                            DateBasedSecurityPatchLevel.fromString(trimmedSpl);
                    if (parsedSpl.compareTo(currentSpl) > 0) {
                        updates.add(new UpdateInfo.Builder()
                                .setComponent(SecurityPatchState.COMPONENT_SYSTEM)
                                .setSecurityPatchLevel(parsedSpl)
                                .setPublishedDateMillis(item.getReleaseTimestampMillis())
                                .setLastCheckTimeMillis(System.currentTimeMillis())
                                .build());
                    }
                }
            }

            // If no advancing SYSTEM update is available (or if a previously offered update was revoked),
            // proactively unregister any cached record for this component.
            if (updates.isEmpty()) {
                UpdateInfo clearTarget = new UpdateInfo.Builder()
                        .setComponent(SecurityPatchState.COMPONENT_SYSTEM)
                        .build();
                updateInfoManager.unregisterUpdate(clearTarget);
            }
            return Futures.immediateFuture(updates);
        } catch (Exception e) {
            return Futures.immediateFailedFuture(e);
        }
    }

    @Override
    protected boolean shouldFetchUpdates() {
        long lastCheckMillis = updateInfoManager.getLastCheckTimeMillis();
        long dataAge = System.currentTimeMillis() - lastCheckMillis;
        return dataAge > TimeUnit.HOURS.toMillis(4);
    }
}

ステップ 4: デバイスの再起動後に適用されたアップデートをクリアする

registerUpdate() が呼び出されるたびに UpdateInfoManager が自動的に古いアップデートを削除しますが、OTA アップデートのインストールが完了すると、次のサーバー同期のスケジュール サイクルまで、アップデータは registerUpdate() を再度呼び出しません。再起動直後に、インストール済みのアップデートが保留中としてクライアント アプリに表示されないようにするには、ACTION_BOOT_COMPLETED をリッスンし、OTA アップデートのインストールが完了したら UpdateInfoManager.unregisterUpdate() を呼び出して、ローカル キャッシュからレコードをクリアします。アップデートのインストールが完了した場合にのみこれを行うことで、通常のデバイスの再起動のたびに保留中の(インストールされていない)アップデートを無条件でワイプすることを回避できます。UpdateInfoManager キーはコンポーネントごとにレコードを更新するため、登録解除用の UpdateInfo オブジェクトを構築する際に、ターゲット コンポーネントを指定するだけで済みます。

Kotlin

// Build target identifying the component to unregister
val target = UpdateInfo.Builder()
    .setComponent(SecurityPatchState.COMPONENT_SYSTEM)
    .build()
// Unregister the update to remove it from disk cache
updateInfoManager.unregisterUpdate(target)
// Refresh last check timestamp to indicate up-to-date state
updateInfoManager.setLastCheckTimeMillis(System.currentTimeMillis())

Java

// Build target identifying the component to unregister
UpdateInfo target = new UpdateInfo.Builder()
    .setComponent(SecurityPatchState.COMPONENT_SYSTEM)
    .build();
// Unregister the update to remove it from disk cache
updateInfoManager.unregisterUpdate(target);
// Refresh last check timestamp to indicate up-to-date state
updateInfoManager.setLastCheckTimeMillis(System.currentTimeMillis());

ステップ 5: 統合を確認する

Android Debug Bridge(ADB)を使用して Android デバイスまたはエミュレータで次のチェックを実行し、エンドツーエンドの統合を検証して、OEM デプロイの一般的な落とし穴を回避します。

  1. クライアントがプロバイダを信頼していることを確認する: クライアント アプリは、READ_PRIVILEGED_PHONE_STATE を保持していないプロバイダを無視します。プリインストールされている場合でも同様です。権限が付与されていることを確認します。

    adb shell dumpsys package <your_package_name> | grep "READ_PRIVILEGED_PHONE_STATE: granted=true"
    

    次に、サービスが検出可能で、サービス権限がないことを確認します。サービスの出力で、exported=true と permission=null を確認します。

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

    それでもクライアントにプロバイダが表示されない場合は、SecurityPatchState タグの Ignoring untrusted update provider を logcat で確認します。

  2. ユーザー 0 と仕事用プロファイルの両方でインテント解決を検証する: Android OS PackageManager が、メインユーザー(User 0)とアクティブな Android Enterprise 仕事用プロファイル(User 10 など)の両方で、エクスポートされた UPDATE_INFO_SERVICE インテント フィルタを解決することをアサートします。

    adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICE
    adb shell pm query-services --user 10 -a androidx.security.state.provider.UPDATE_INFO_SERVICE
    
  3. dumpsys を使用してサービスの状態とキャッシュに保存されたレコードを確認する: UpdateInfoService は dump() をオーバーライドして、Global Last Check、Should Throttle(レート制限の状態)、Cached Updates をレポートします。UpdateInfoService はバインドされたサービスであり、クライアントはクエリの直後にバインドを解除するため、クライアントがバインドされていない場合、dumpsys activity service は (nothing) を出力します。dumpsys を実行する前に、サービスを明示的に起動します。

    adb shell am start-service -a androidx.security.state.provider.UPDATE_INFO_SERVICE <your_package_name>/.<service_class_name>
    adb shell dumpsys activity service <your_package_name>/.<service_class_name>
    

    診断出力の例:

    UpdateInfoService State:
      Active Requests: 0
      Global Last Check: Thu Jan 01 12:00:00 UTC 2026
      Should Throttle: false
      Cached Updates (1):
        - Component: SYSTEM
          SPL: 2026-01-01
          Published: Thu Jan 01 00:00:00 UTC 2026
          Last Checked: Thu Jan 01 12:00:00 UTC 2026
    
  4. クライアント バインディングをトリガーし、テレメトリーの結果を検証する: 権限のないテストアプリ(システム署名権限を保持していない)から SecurityPatchState.queryAllAvailableUpdates() を呼び出します。テレメトリー コールバックを実装した場合は、次の点を確認します。

    • 権限のないクライアントが SecurityException なしでバインドし、onClientConnected(packageName, callerUid) をトリガーすることを確認します。
    • プッシュモデル プロバイダ(shouldFetchUpdates() == false)の場合: すべてのクエリで onRequestCompleted(telemetry) が fetchDurationMillis == 0 を使用して UpdateFetchOutcome.CACHE_HIT(1)をログに記録していることを確認します。
    • プルモデル プロバイダ(shouldFetchUpdates() == true)の場合: onRequestCompleted(telemetry) ログが、最初の古いキャッシュクエリで UpdateFetchOutcome.FETCHED(3)を記録し、その直後のクエリで CACHE_HIT(1)を記録していることを確認します。(Pull モデルのテスト実行間で 1 時間の永続レート制限をリセットするには、adb shell pm clear <your_package_name> を実行します)。

オプションと高度な構成

キャッシュ ポリシーとレート制限

クライアントが更新をクエリすると、UpdateInfoService は二重チェック ロック ワークフローを実行して、データの鮮度とバックエンド サーバーの負荷のバランスを取ります。

UpdateInfoService は、二重チェック ロック ワークフローを実行して、データの鮮度とバックエンド サーバーの負荷のバランスを取ります

  • 高速パス(shouldFetchUpdates()): デフォルトでは、shouldFetchUpdates() は、グローバル lastCheckTimeMillis が 1 時間(TimeUnit.HOURS.toMillis(1))より古い場合にのみ true(古いキャッシュを示す)を返します。shouldFetchUpdates() が false を返すと、サービスはロックを取得したり、ネットワーク I/O を実行したりせずに、結果 UpdateFetchOutcome.CACHE_HIT のキャッシュされたレコードを直ちに返します。shouldFetchUpdates() をオーバーライドして、このキャッシュ保存ポリシーをカスタマイズできます。
  • スローパスとリクエストの統合: shouldFetchUpdates() が true を返すと、サービスは内部コルーチン ミューテックスを取得し、shouldFetchUpdates() を再評価します(同時リクエストがロックの待機中にすでにキャッシュを更新している場合は UpdateFetchOutcome.COALESCED を返します)。
  • 永続的なレート制限(shouldThrottle()): バックエンド インフラストラクチャをクエリ バーストや繰り返しの障害から保護するため、shouldThrottle() はアプリとデバイスの再起動にわたって 1 時間以上の最小間隔を強制します。UpdateInfoService は fetchUpdates() を呼び出す前に各試行を記録するため、fetchUpdates() が例外をスローした場合(onFetchFailed(e) の呼び出し後に UpdateFetchOutcome.FAILED を返す場合)、次の 60 分間の後続のクエリは、結果 UpdateFetchOutcome.THROTTLED でキャッシュに保存されたフォールバック データを正常に返します。

オブザーバビリティ、テレメトリー、診断

UpdateInfoService は、クライアントの導入状況の追跡、IPC レイテンシのモニタリング、低レベルの AIDL スタブの計測を行わずにバックエンド エラーのロギングを行うための組み込みのオブザーバビリティ フックを提供します。

UpdateInfoService(Kotlin)または ListenableFutureUpdateInfoService(Java)でこれらのコールバックをオーバーライドします。

Kotlin

import androidx.security.state.provider.UpdateCheckTelemetry
import androidx.security.state.provider.UpdateFetchOutcome
import androidx.security.state.provider.UpdateInfoService

abstract class MonitoredUpdateInfoService : UpdateInfoService() {
    override fun onRequestCompleted(telemetry: UpdateCheckTelemetry) {
        val outcomeName = when (telemetry.outcome) {
            UpdateFetchOutcome.CACHE_HIT -> "CACHE_HIT"
            UpdateFetchOutcome.COALESCED -> "COALESCED"
            UpdateFetchOutcome.FETCHED -> "FETCHED"
            UpdateFetchOutcome.THROTTLED -> "THROTTLED"
            UpdateFetchOutcome.FAILED -> "FAILED"
            else -> "UNKNOWN"
        }
        MyAnalytics.logEvent("SECURITY_UPDATE_CHECK")
            .addParam("outcome", outcomeName)
            .addParam("total_duration_ms", telemetry.totalDurationMillis)
            .addParam("lock_wait_ms", telemetry.lockWaitDurationMillis)
            .addParam("processing_ms", telemetry.processingDurationMillis)
            .addParam("fetch_duration_ms", telemetry.fetchDurationMillis)
            .addParam("caller_uid", telemetry.callerUid)
            .send()
    }

    override fun onClientConnected(packageName: String, callerUid: Int) {
        // Track authenticated client sessions and adoption
        MyMetrics.incrementCounter("client_connected", "package", packageName)
    }

    override fun onClientDisconnected(packageName: String, callerUid: Int) {
        // Track session termination and cleanup resources
        MyMetrics.incrementCounter("client_disconnected", "package", packageName)
    }

    override fun onFetchFailed(e: Exception) {
        // Report exceptions caught during the update check workflow
        MyCrashReporter.recordException(e)
    }
}

Java

import androidx.annotation.NonNull;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import androidx.security.state.provider.UpdateCheckTelemetry;
import androidx.security.state.provider.UpdateFetchOutcome;

public abstract class MonitoredUpdateInfoService extends ListenableFutureUpdateInfoService {
    @Override
    protected void onRequestCompleted(@NonNull UpdateCheckTelemetry telemetry) {
        String outcomeName;
        switch (telemetry.getOutcome()) {
            case UpdateFetchOutcome.CACHE_HIT: outcomeName = "CACHE_HIT"; break;
            case UpdateFetchOutcome.COALESCED: outcomeName = "COALESCED"; break;
            case UpdateFetchOutcome.FETCHED: outcomeName = "FETCHED"; break;
            case UpdateFetchOutcome.THROTTLED: outcomeName = "THROTTLED"; break;
            case UpdateFetchOutcome.FAILED: outcomeName = "FAILED"; break;
            default: outcomeName = "UNKNOWN"; break;
        }
        MyAnalytics.logEvent("SECURITY_UPDATE_CHECK")
            .addParam("outcome", outcomeName)
            .addParam("total_duration_ms", telemetry.getTotalDurationMillis())
            .addParam("lock_wait_ms", telemetry.getLockWaitDurationMillis())
            .addParam("processing_ms", telemetry.getProcessingDurationMillis())
            .addParam("fetch_duration_ms", telemetry.getFetchDurationMillis())
            .addParam("caller_uid", telemetry.getCallerUid())
            .send();
    }

    @Override
    protected void onClientConnected(@NonNull String packageName, int callerUid) {
        MyMetrics.incrementCounter("client_connected", "package", packageName);
    }

    @Override
    protected void onClientDisconnected(@NonNull String packageName, int callerUid) {
        MyMetrics.incrementCounter("client_disconnected", "package", packageName);
    }

    @Override
    protected void onFetchFailed(@NonNull Exception e) {
        MyCrashReporter.recordException(e);
    }
}

テレメトリーの結果とレイテンシ指標

UpdateCheckTelemetry は単調経過時間(SystemClock.elapsedRealtime())を測定し、UpdateFetchOutcome で定義された 5 つの結果のいずれかを報告します。

結果定数 @IntDef コード 記録される指標のプロパティ 説明とシステムの状態
UpdateFetchOutcome.CACHE_HIT 1 totalDurationMillis、processingDurationMillis、callerUid 高速パスのローカル ディスク/メモリ キャッシュから直ちに提供されます(shouldFetchUpdates() が false を返します)。lockWaitDurationMillis と fetchDurationMillis は 0 です。
UpdateFetchOutcome.COALESCED 2 totalDurationMillis、lockWaitDurationMillis、processingDurationMillis、callerUid 別の有効な更新の後にキューに登録されたクエリ。ロックを取得すると、データは最新の状態でした。重複するネットワーク フェッチを回避(fetchDurationMillis は 0)。
UpdateFetchOutcome.FETCHED 3 totalDurationMillis、lockWaitDurationMillis、processingDurationMillis、fetchDurationMillis、callerUid バックエンド ネットワークの同期が正常に実行されました(fetchUpdates() が完了しました)。新しいレコードがディスクに永続化されました。
UpdateFetchOutcome.THROTTLED 4 totalDurationMillis、lockWaitDurationMillis、processingDurationMillis、callerUid レート制限によりリクエストがブロックされました(shouldThrottle() が true を返しました)。キャッシュに保存されているデータがクライアントに安全に返されました(fetchDurationMillis が 0 です)。
UpdateFetchOutcome.FAILED 5 totalDurationMillis、lockWaitDurationMillis、processingDurationMillis、fetchDurationMillis、callerUid アップデートの確認またはネットワーク リクエストで例外が発生しました。例外ファイアウォールでキャッチされ、onFetchFailed(e) が起動し、キャッシュに保存されたフォールバックが返されました。

高度なサービス ブローカー フック: getCallerUid()

クライアントが接続すると、UpdateInfoService は最初の Binder スレッドで getCallerUid() を自動的に評価し(fetchUpdates() の前に Binder.clearCallingIdentity() を呼び出す前)、パッケージの所有権を確認し、確認済みの呼び出し元 UID を onRequestCompleted(telemetry) の onClientConnected()、onClientDisconnected()、telemetry.callerUid に直接渡します。

標準の Android <service> コンポーネントの場合、getCallerUid() を呼び出したりオーバーライドしたりする必要はありません。protected open getCallerUid() メソッド(デフォルトでは Binder.getCallingUid() に委任)は、内部サービス ブローカーまたはプロキシ アーキテクチャを介して Binder IPC をルーティングするホストアプリのオーバーライド フックとして提供され、サブクラスがブローカーの UID ではなく論理クライアント UID を返すことができます。

参考情報

セキュリティ状態の公開の詳細については、次のリソースをご覧ください。

ドキュメント

API リファレンス