डिवाइस की सुरक्षा की स्थिति पब्लिश करना

अगर आप ओरिजनल इक्विपमेंट मैन्युफ़ैक्चरर (ओईएम) हैं या आपके पास ओवर-द-एयर (OTA) अपडेट क्लाइंट को मैनेज करने का खास अधिकार है, तो डिवाइस पर मौजूद सुरक्षा से जुड़े संवेदनशील ऐप्लिकेशन को, सुरक्षा से जुड़े लंबित अपडेट की जानकारी दी जा सकती है. इससे वे डिवाइस की सुरक्षा की स्थिति का सही आकलन कर पाएंगे. ज़्यादा भरोसेमंद ज़ीरो-ट्रस्ट नीतियां लागू करने के लिए, ऐप्लिकेशन को यह पुष्टि करनी होगी कि डिवाइस पर इंस्टॉल किया गया सुरक्षा पैच का लेवल (डिवाइस सिक्योरिटी पैच लेवल या डीएसपीएल) क्या है. साथ ही, यह भी पुष्टि करनी होगी कि सुरक्षा से जुड़े कौनसे अपडेट उपलब्ध हैं और इंस्टॉल करने के लिए तैयार हैं (उपलब्ध सिक्योरिटी पैच लेवल या एएसपीएल).

बिना खास अधिकारों वाले क्लाइंट ऐप्लिकेशन, फ़र्मवेयर की प्रॉपर्टी को सीधे तौर पर नहीं पढ़ सकते. साथ ही, वे अपडेटर के निजी डेटाबेस की जांच नहीं कर सकते या ओईएम के इंटरनल बैकएंड एंडपॉइंट से क्वेरी नहीं कर सकते. इसलिए, AndroidX Security State Provider लाइब्रेरी, इंटरप्रोसेस कम्यूनिकेशन (आईपीसी) का स्टैंडर्ड और सुरक्षित आर्किटेक्चर उपलब्ध कराती है. अपडेट क्लाइंट, इसका इस्तेमाल उपलब्ध अपडेट के बारे में जानकारी शेयर करने के लिए करते हैं. OTA अपडेट क्लाइंट में UpdateInfoService लागू करके, सिस्टम के लिए ASPL मेटाडेटा पब्लिश किया जा सकता है. इसके लिए, मालिकाना हक वाले बैकएंड इंटिग्रेशन को सार्वजनिक करने की ज़रूरत नहीं होती. Google, GMS डिवाइसों के लिए मॉड्यूलर सिस्टम कॉम्पोनेंट (Mainline) अपडेटर उपलब्ध कराता है. हालांकि, नॉन-GMS डिवाइस भी इन मॉड्यूलर सिस्टम कॉम्पोनेंट के लिए ASPL मेटाडेटा पब्लिश कर सकते हैं.

आर्किटेक्चर की खास जानकारी

नीचे दिए गए डायग्राम में बताया गया है कि AndroidX Security State Provider लाइब्रेरी, बिना खास अधिकारों वाले क्लाइंट ऐप्लिकेशन और उपयोगकर्ता के डिवाइस पर अपडेट की जाने वाली सेवाओं के बीच, स्टैंडर्ड और सुरक्षित आईपीसी फ़्रेमवर्क कैसे बनाती है:

AndroidX Security State Provider लाइब्रेरी, बिना किसी खास अधिकार वाले क्लाइंट ऐप्लिकेशन और उपयोगकर्ता के डिवाइस पर अपडेट करने वाली सेवाओं के बीच, स्टैंडर्ड और सुरक्षित आईपीसी फ़्रेमवर्क बनाती है

डेटा डिलीवरी मॉडल

क्लाइंट ऐप्लिकेशन, queryAllAvailableUpdates या fetchAvailableSecurityPatchLevel को कॉल करके, क्वेरी अपडेट होने की उपलब्धता के बारे में जानकारी देते हैं. क्लाइंट लाइब्रेरी, रजिस्टर की गई उन सभी सेवाओं का अपने-आप पता लगाती है और उनसे जुड़ जाती है जो UpdateInfoService क्लास को डिवाइस पर सिस्टम ऐप्लिकेशन से बढ़ाती हैं. इन ऐप्लिकेशन के पास READ_PRIVILEGED_PHONE_STATE अनुमति होती है.

ऊपर दिए गए डायग्राम में दिखाया गया है कि security-state-provider लाइब्रेरी, डेटा डिलीवर करने के दो मॉडल के साथ काम करती है:

डिलीवरी मॉडल सिंक्रनाइज़ेशन ट्रिगर क्लाइंट का जवाब इस्तेमाल के सुझाए गए उदाहरण
पुश मॉडल (बैकग्राउंड सिंक) शेड्यूल किए गए बैकग्राउंड वर्कर (WorkManager या JobScheduler), आपके बैकएंड के साथ सिंक होते हैं और UpdateInfoManager में रिकॉर्ड लिखते हैं. आपकी सेवा हमेशा लोकल डिस्क की कैश मेमोरी (shouldFetchUpdates() = false) से कॉन्टेंट दिखाती है. स्थानीय कैश मेमोरी से तुरंत दिखाया जाता है. ओईएम सिस्टम के ओटीए अपडेटर और बैकग्राउंड में सिंक किए गए मॉड्यूलर कॉम्पोनेंट अपडेटर.
पुल मॉडल (मांग पर सिंक करना) जब कैश मेमोरी में सेव किए गए रिकॉर्ड पुराने हो जाते हैं (shouldFetchUpdates() = true), तब क्लाइंट आईपीसी क्वेरी आने पर नेटवर्क फ़ेच ट्रिगर होता है. म्यूटेक्स कोलेसिंग और रेट लिमिटिंग (shouldThrottle()) की मदद से, आपके बैकएंड को स्पाइक से सुरक्षित रखा जाता है. यह कुकी, पुरानी हो चुकी कैश मेमोरी के लिए बैकएंड फ़ेच का इंतज़ार करती है. शेड्यूल किए गए बैकग्राउंड सिंक वर्कर के बिना, मोनोलिथिक ओईएम ओटीए अपडेटर.

अपडेट देने वाली कई कंपनियां

प्रोडक्शन Android डिवाइसों पर, अपडेट देने वाली कई कंपनियां एक साथ काम करती हैं. उदाहरण के लिए, Mainline, मॉड्यूलर कॉम्पोनेंट (COMPONENT_SYSTEM_MODULES) के लिए उपलब्धता की जानकारी पब्लिश करता है. वहीं, आपका OEM OTA क्लाइंट, प्राइमरी ओएस इमेज (COMPONENT_SYSTEM) के लिए अपडेट पब्लिश करता है.

आपकी सेवा को सिर्फ़ उन कॉम्पोनेंट के अपडेट रजिस्टर करने होंगे जिन्हें वह मैनेज करती है. अगर किसी डिवाइस पर कई प्रोवाइडर, एक ही कॉम्पोनेंट के लिए अपडेट पब्लिश करते हैं, तो क्लाइंट ऐप्लिकेशन, सबसे ज़्यादा उपलब्ध पैच लेवल (fetchAvailableSecurityPatchLevel() का इस्तेमाल करके) का आकलन करते हैं. इसके अलावा, वे एंटरप्राइज़ ऑडिटिंग के लिए, अलग-अलग UpdateInfo रिकॉर्ड (queryAllAvailableUpdates() का इस्तेमाल करके) की जांच करते हैं. पक्का करें कि आपकी सेवा, आपके कॉम्पोनेंट के लिए हमेशा कैननिकल फ़ॉर्मैट (COMPONENT_SYSTEM के लिए DateBasedSecurityPatchLevel) पब्लिश करती हो.

अपडेट क्लाइंट को ऑनबोर्ड करने के लिए सिलसिलेवार निर्देश

AndroidX Security State Provider लाइब्रेरी को अपडेट क्लाइंट में इंटिग्रेट करने के लिए, यह तरीका अपनाएं. साथ ही, अपने डिवाइस के सुरक्षा अपडेट की उपलब्धता को पब्लिश करना शुरू करें.

पहला चरण: डिपेंडेंसी जोड़ना

अपडेट प्रोवाइडर को लागू करने के लिए, पक्का करें कि आपके प्रोजेक्ट में 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'
}

दूसरा चरण: मेनिफ़ेस्ट फ़ाइल में अपडेट सेवा के बारे में जानकारी देना

अपने ऐप्लिकेशन के AndroidManifest.xml में अपनी सेवा के बारे में बताएं. इसके लिए, <intent-filter> से मेल खाने वाला androidx.security.state.provider.UPDATE_INFO_SERVICE इस्तेमाल करें. सेवा को एक्सपोर्ट (android:exported="true") किया जाना चाहिए और एक उपयोगकर्ता वाली सेवा (android:singleUser="true") के तौर पर कॉन्फ़िगर किया जाना चाहिए, ताकि क्लाइंट लाइब्रेरी इसे प्रोसेस और उपयोगकर्ता की सीमाओं के हिसाब से बाइंड कर सके. खास तौर पर, वर्क प्रोफ़ाइलों के लिए:

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

अगर आपका अपडेटर android.uid.system के तौर पर नहीं चलता है, तो अपने मेनिफ़ेस्ट में यहां दी गई अनुमतियों का एलान करें. साथ ही, पक्का करें कि आपने उन्हें अनुमति वाली अनुमतियों की अनुमति वाली सूची में जोड़ा हो:

  • READ_PRIVILEGED_PHONE_STATE: क्लाइंट को आपके सेवा देने वाले पर भरोसा करने के लिए ज़रूरी है.
  • INTERACT_ACROSS_USERS: android:singleUser="true" के लिए ज़रूरी है.

तीसरा चरण: UpdateInfoService लागू करना

अपडेट का स्टेटस पब्लिश करने के लिए, आपको UpdateInfoService क्लास लागू करनी होगी. साथ ही, अपडेट के ऐसे रिकॉर्ड बनाने होंगे जो अनुमानित डेटा मॉडल से मेल खाते हों.

UpdateInfo डेटा मॉडल स्पेसिफ़िकेशन

चाहे पुश मॉडल चुना गया हो या पुल मॉडल, UpdateInfo रिकॉर्ड बनाएं. इसके लिए, UpdateInfo.Builder का इस्तेमाल करें. रिकॉर्ड, यहां दी गई खास जानकारी के मुताबिक होने चाहिए:

फ़ील्ड का नाम Getter Method डेटा टाइप पुष्टि करने और फ़ॉर्मैट से जुड़ी ज़रूरी शर्तें मकसद और सिस्टम सिमैंटिक
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 epoch के बाद के मिलीसेकंड (System.currentTimeMillis()). यह > 0 होना चाहिए. अपडेट को उपयोगकर्ताओं के लिए कब उपलब्ध कराया गया था. जैसे, ओटीए रिलीज़ का समय. पे लोड डाउनलोड या इंस्टॉल करने में लगने वाले समय का इस्तेमाल न करें.
lastCheckTimeMillis getLastCheckTimeMillis() long यूनिक्स टाइम (मिलीसेकंड में). > 0 होना चाहिए. टाइमस्टैंप, जब स्वास्थ्य सेवा देने वाली कंपनी या संगठन ने सिंक करने के दौरान इस अपडेट रिकॉर्ड की पुष्टि की या इसका पता लगाया.

नीचे दिए गए विकल्पों में से, ऐसा डिलीवरी मॉडल चुनें जो आपके अपडेटर के आर्किटेक्चर के साथ काम करता हो:

पहला विकल्प: पुश मॉडल (सुझाया गया)

जब बैकग्राउंड सिंक वर्कर, आपके ओटीए सर्वर की जांच करता है, तो पुष्टि करें कि खोजा गया कोई भी अपडेट, डिवाइस के मौजूदा पैच लेवल को बेहतर बनाता है. साथ ही, UpdateInfoManager.registerUpdate() का इस्तेमाल करके इसे सेव करें. अगर सुरक्षा से जुड़ा कोई अपडेट बाकी नहीं है, तो UpdateInfoManager.unregisterUpdate() को कॉल करें. हर सिंक के आखिर में हमेशा UpdateInfoManager.setLastCheckTimeMillis() को कॉल करें. भले ही, आपने 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 में अपडेट रिकॉर्ड करते हैं. फ़्रेमवर्क को हमेशा लोकल डिस्क स्टोरेज से रिकॉर्ड दिखाने का निर्देश देने के लिए, shouldFetchUpdates() को बदलें. इसके लिए, Kotlin में UpdateInfoService या Java में ListenableFutureUpdateInfoService को एक्सटेंड करके 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 (सस्पेंड करने वाले 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);
    }
}

चौथा चरण: डिवाइस को रीबूट करने के बाद, लागू किए गए अपडेट मिटाना

UpdateInfoManager, registerUpdate() के चालू होने पर, पुराने अपडेट अपने-आप हटा देता है. हालांकि, ओटीए अपडेट इंस्टॉल होने के बाद, अपडेटर registerUpdate() को तब तक दोबारा कॉल नहीं करेगा, जब तक कि सर्वर सिंक करने का अगला शेड्यूल पूरा नहीं हो जाता. रीबूट करने के तुरंत बाद, क्लाइंट ऐप्लिकेशन को पहले से इंस्टॉल किया गया अपडेट, अब भी लंबित के तौर पर न दिखे, इसके लिए ACTION_BOOT_COMPLETED को सुनें. साथ ही, जब ओटीए अपडेट इंस्टॉल हो जाए, तब 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());

पांचवां चरण: इंटिग्रेशन की पुष्टि करना

एंड-टू-एंड इंटिग्रेशन की पुष्टि करने के लिए, Android डिवाइस या एम्युलेटर पर Android Debug Bridge (ADB) का इस्तेमाल करके, यहां दी गई जांचें करें. साथ ही, ओईएम के डिप्लॉयमेंट से जुड़ी सामान्य समस्याओं से बचें:

  1. पुष्टि करें कि क्लाइंट को सेवा देने वाली कंपनी पर भरोसा है: क्लाइंट ऐप्लिकेशन, सेवा देने वाली ऐसी किसी भी कंपनी को अनदेखा करते हैं जिसके पास READ_PRIVILEGED_PHONE_STATE नहीं है. भले ही, वह पहले से इंस्टॉल हो. पुष्टि करें कि अनुमति दी गई है:

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

    इसके बाद, पुष्टि करें कि आपकी सेवा को खोजा जा सकता है और उसके लिए किसी अनुमति की ज़रूरत नहीं है. अपनी सेवा के आउटपुट में, exported=true और permission=null देखें:

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

    अगर क्लाइंट को अब भी आपका प्रोवाइडर नहीं दिखता है, तो SecurityPatchState टैग से Ignoring untrusted update provider के लिए logcat देखें.

  2. User 0 और Work Profile, दोनों में इंटेंट रिज़ॉल्यूशन की पुष्टि करें: पुष्टि करें कि Android OS PackageManager, एक्सपोर्ट किए गए UPDATE_INFO_SERVICE इंटेंट फ़िल्टर को प्राइमरी यूज़र (User 0) और Android Enterprise की किसी भी चालू Work Profile (जैसे कि User 10) में हल करता है:

    adb shell pm query-services --user 0 -a androidx.security.state.provider.UPDATE_INFO_SERVICE
    adb shell pm query-services --user 10 -a androidx.security.state.provider.UPDATE_INFO_SERVICE
    
  3. dumpsys का इस्तेमाल करके, सेवा की स्थिति और कैश मेमोरी में सेव किए गए रिकॉर्ड की पुष्टि करें: UpdateInfoService, Global Last Check, Should Throttle (रेट लिमिटर की स्थिति) और Cached Updates की रिपोर्ट करने के लिए, dump() को बदलता है. 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) लॉग UpdateFetchOutcome.CACHE_HIT (1) में हर क्वेरी के लिए fetchDurationMillis == 0 मौजूद है.
    • पुल-मॉडल की सुविधा देने वाली कंपनियों (shouldFetchUpdates() == true) के लिए: पुष्टि करें कि onRequestCompleted(telemetry) लॉग UpdateFetchOutcome.FETCHED (3) शुरुआती पुरानी कैश मेमोरी वाली क्वेरी पर, इसके बाद CACHE_HIT (1) तुरंत बाद की क्वेरी पर. (पुल-मॉडल के टेस्ट रन के बीच, एक घंटे के लिए लगातार काम करने वाले रेट लिमिटर को रीसेट करने के लिए, adb shell pm clear <your_package_name> चलाएं.)

वैकल्पिक और ऐडवांस कॉन्फ़िगरेशन

कैश मेमोरी में सेव करने की नीति और अनुरोध संख्या सीमित करना

जब कोई क्लाइंट अपडेट के बारे में क्वेरी करता है, तो UpdateInfoService, डबल-चेक किए गए लॉकिंग वर्कफ़्लो को लागू करता है. इससे बैकएंड सर्वर के लोड के हिसाब से, डेटा अपडेट होने की फ़्रीक्वेंसी को बैलेंस किया जा सकता है:

UpdateInfoService, डबल-चेक किए गए लॉकिंग वर्कफ़्लो को लागू करता है. इससे बैकएंड सर्वर पर लोड कम होता है और डेटा लगातार अपडेट होता रहता है

  • फ़ास्ट पाथ (shouldFetchUpdates()): डिफ़ॉल्ट रूप से, shouldFetchUpdates() सिर्फ़ तब true दिखाता है, जब ग्लोबल lastCheckTimeMillis एक घंटे से ज़्यादा पुराना हो (TimeUnit.HOURS.toMillis(1)). जब shouldFetchUpdates(), false दिखाता है, तो सेवा तुरंत कैश मेमोरी में सेव किए गए रिकॉर्ड दिखाती है. इसमें UpdateFetchOutcome.CACHE_HIT नतीजे मिलते हैं. इसके लिए, लॉक हासिल करने या नेटवर्क I/O करने की ज़रूरत नहीं होती. कैशिंग की इस नीति को अपनी पसंद के मुताबिक बनाने के लिए, shouldFetchUpdates() को बदला जा सकता है.
  • स्लो पाथ और अनुरोधों को एक साथ प्रोसेस करना: जब shouldFetchUpdates(), true दिखाता है, तो सेवा एक इंटरनल कोरूटीन म्यूटेक्स हासिल करती है और shouldFetchUpdates() का फिर से आकलन करती है. अगर लॉक का इंतज़ार करते समय, किसी साथ-साथ किए गए अनुरोध ने पहले ही कैश मेमोरी को रीफ़्रेश कर दिया है, तो UpdateFetchOutcome.COALESCED दिखाता है.
  • परसिस्टेंट रेट लिमिटर (shouldThrottle()): बैकएंड इन्फ़्रास्ट्रक्चर को क्वेरी के अचानक बढ़ने या बार-बार होने वाली गड़बड़ियों से बचाने के लिए, shouldThrottle() ऐप्लिकेशन और डिवाइस को रीस्टार्ट करने के दौरान, कम से कम एक घंटे का इंटरवल लागू करता है. UpdateInfoService, fetchUpdates() को शुरू करने से पहले हर कोशिश को रिकॉर्ड करता है. इसलिए, अगर fetchUpdates() कोई अपवाद दिखाता है (onFetchFailed(e) को शुरू करने के बाद UpdateFetchOutcome.FAILED दिखाता है), तो अगले 60 मिनट के दौरान की गई क्वेरी, UpdateFetchOutcome.THROTTLED नतीजे के साथ कैश मेमोरी में सेव किए गए फ़ॉलबैक डेटा को दिखाती हैं.

ऑब्ज़र्वअबिलिटी, टेलीमेट्री, और डाइग्नोस्टिक्स

UpdateInfoService में, क्लाइंट के इस्तेमाल को ट्रैक करने, आईपीसी की लेटेन्सी पर नज़र रखने, और एआईडीएल स्टब के निचले स्तर के इंस्ट्रुमेंटेशन के बिना बैकएंड की गड़बड़ियों को लॉग करने के लिए, पहले से मौजूद ऑब्ज़र्वेबिलिटी हुक उपलब्ध कराए जाते हैं:

  • onRequestCompleted(telemetry): इस फ़ंक्शन को हर अपडेट की जांच पूरी होने पर कॉल किया जाता है. इसमें UpdateCheckTelemetry की खास जानकारी शामिल होती है.
  • onClientConnected(packageName, callerUid): इस फ़ंक्शन को तब शुरू किया जाता है, जब पुष्टि किया गया कोई क्लाइंट सेशन खोलता है.
  • onClientDisconnected(packageName, callerUid): इस फ़ंक्शन को तब शुरू किया जाता है, जब कोई क्लाइंट अनबाइंड करता है या उसकी प्रोसेस बंद हो जाती है.
  • onFetchFailed(e): इस फ़ंक्शन को तब कॉल किया जाता है, जब fetchUpdates() के दौरान कोई अपवाद होता है. ऐसा तब होता है, जब सेवा, कैश मेमोरी में सेव किया गया फ़ॉलबैक डेटा दिखाती है.

इन कॉलबैक को UpdateInfoService (Kotlin) या ListenableFutureUpdateInfoService (Java) में बदलें:

Kotlin

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

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

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

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

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

Java

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

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

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

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

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

टेलीमेट्री के नतीजे और लेटेंसी मेट्रिक

UpdateCheckTelemetry, समय के साथ लगातार बढ़ने वाली अवधि को मापता है (SystemClock.elapsedRealtime()). साथ ही, UpdateFetchOutcome में तय किए गए पांच नतीजों में से किसी एक की रिपोर्ट करता है:

नतीजा कॉन्सटेंट @IntDef कोड लॉग की गई मेट्रिक प्रॉपर्टी ब्यौरा और सिस्टम की स्थिति
UpdateFetchOutcome.CACHE_HIT 1 totalDurationMillis, processingDurationMillis, callerUid इसे फ़ास्ट पाथ (shouldFetchUpdates() लौटाया गया false) पर, लोकल डिस्क/डिवाइस की कैश मेमोरी से तुरंत दिखाया जाता है. lockWaitDurationMillis और fetchDurationMillis, 0 हैं.
UpdateFetchOutcome.COALESCED 2 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid क्वेरी को किसी अन्य रीफ़्रेश के पीछे रखा गया है. लॉक हासिल करने पर, डेटा नया था. डुप्लीकेट नेटवर्क फ़ेच से बचा गया (fetchDurationMillis, 0 है).
UpdateFetchOutcome.FETCHED 3 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid बैकएंड नेटवर्क सिंक हो गया है (fetchUpdates() पूरा हुआ). डिस्क में नए रिकॉर्ड सेव किए गए.
UpdateFetchOutcome.THROTTLED 4 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, callerUid रेट लिमिटर ने अनुरोध को ब्लॉक कर दिया (shouldThrottle() ने true दिखाया). क्लाइंट को कैश मेमोरी में सेव किया गया डेटा सुरक्षित तरीके से भेजा गया (fetchDurationMillis, 0 है).
UpdateFetchOutcome.FAILED 5 totalDurationMillis, lockWaitDurationMillis, processingDurationMillis, fetchDurationMillis, callerUid अपडेट की जांच या नेटवर्क के अनुरोध में कोई गड़बड़ी हुई. इसे फ़ायरवॉल के अपवाद ने पकड़ा. onFetchFailed(e) को ट्रिगर किया गया और कैश मेमोरी में सेव किया गया फ़ॉलबैक वापस मिला.

एडवांस सर्विस ब्रोकर हुक: getCallerUid()

जब कोई क्लाइंट कनेक्ट होता है, तो UpdateInfoService शुरुआती बाइंडर थ्रेड पर getCallerUid() का आकलन अपने-आप करता है. यह आकलन, fetchUpdates() से पहले Binder.clearCallingIdentity() को कॉल करने से पहले किया जाता है. इसके बाद, यह पैकेज के मालिकाना हक की पुष्टि करता है और पुष्टि किए गए कॉलर यूआईडी को सीधे onClientConnected(), onClientDisconnected(), और telemetry.callerUid को onRequestCompleted(telemetry) में पास करता है.

स्टैंडर्ड Android <service> कॉम्पोनेंट के लिए, आपको getCallerUid() को कॉल या ओवरराइड करने की ज़रूरत नहीं है. protected open getCallerUid() तरीका (जो डिफ़ॉल्ट रूप से Binder.getCallingUid() को सौंपता है) होस्ट ऐप्लिकेशन के लिए ओवरराइड हुक के तौर पर दिया जाता है. ये ऐप्लिकेशन, Binder IPC को इंटरनल सर्विस ब्रोकर या प्रॉक्सी आर्किटेक्चर के ज़रिए रूट करते हैं. इससे सबक्लास, ब्रोकर के यूआईडी के बजाय लॉजिकल क्लाइंट यूआईडी को वापस कर पाता है.

अन्य संसाधन

सुरक्षा की स्थिति को पब्लिश करने के बारे में ज़्यादा जानने के लिए, यहां दिए गए संसाधन देखें:

दस्तावेज़

एपीआई का संदर्भ