ডিভাইসের নিরাপত্তা সংক্রান্ত স্ট্যাটাস প্রকাশ করা

আপনি যদি একজন অরিজিনাল ইকুইপমেন্ট ম্যানুফ্যাকচারার (OEM) হন বা একটি বিশেষ ওভার-দ্য-এয়ার (OTA) আপডেট ক্লায়েন্ট বজায় রাখেন, তাহলে আপনি ডিভাইসে নিরাপত্তা-সংবেদনশীল অ্যাপগুলিকে মুলতুবি থাকা নিরাপত্তা আপডেট সম্পর্কে দৃশ্যমানতা দিতে পারেন যাতে তারা সঠিকভাবে ডিভাইসের নিরাপত্তা সংক্রান্ত অবস্থান মূল্যায়ন করতে পারে। কঠোরভাবে জিরো-ট্রাস্ট নীতি প্রয়োগ করতে, অ্যাপগুলিকে অবশ্যই ডিভাইসে ইনস্টল করা প্যাচ লেভেল (ডিভাইস নিরাপত্তা প্যাচ লেভেল বা DSPL) ছাড়াও, কোন কোন নিরাপত্তা আপডেট উপলভ্য এবং ইনস্টল করার জন্য রেডি (উপলভ্য নিরাপত্তা প্যাচ লেভেল বা ASPL) তা যাচাই করতে হবে।

কারণ, বিশেষ সুবিধা নেই এমন ক্লায়েন্ট অ্যাপ সরাসরি ফার্মওয়্যার প্রপার্টি পড়তে, ব্যক্তিগত আপডেটার ডেটাবেস পরিদর্শন করতে বা ইন্টার্নাল OEM ব্যাকএন্ড এন্ডপয়েন্ট কোয়েরি করতে পারে না। তাই, AndroidX Security State Provider লাইব্রেরি একটি স্ট্যান্ডার্ডাইজড, সুরক্ষিত ইন্টারপ্রসেস কমিউনিকেশন (IPC) আর্কিটেকচার প্রদান করে যা উপলভ্য আপডেট সম্পর্কে তথ্য শেয়ার করতে আপডেট ক্লায়েন্ট ব্যবহার করে। আপনার OTA আপডেট ক্লায়েন্টে UpdateInfoService প্রয়োগ করার মাধ্যমে, আপনি মালিকানাধীন ব্যাকএন্ড ইন্টিগ্রেশন প্রকাশ না করেই সিস্টেমের জন্য ASPL মেটাডেটা প্রকাশ করতে পারবেন। GMS ডিভাইসের জন্য মডুলার সিস্টেম কম্পোনেন্ট (Mainline) আপডেটার প্রয়োগ করার সুবিধা Google দিলেও, GMS নয় এমন ডিভাইসও এইসব মডুলার সিস্টেম কম্পোনেন্টের জন্য ASPL মেটাডেটা প্রকাশ করতে পারে।

আর্কিটেকচার ওভারভিউ

নিচের ডায়াগ্রাম থেকে বোঝা যায় যে কীভাবে AndroidX Security State Provider লাইব্রেরি, বিশেষ সুবিধা নেই এমন ক্লায়েন্ট অ্যাপ এবং ডিভাইসে থাকা আপডেট পরিষেবার মধ্যে একটি স্ট্যান্ডার্ড, সুরক্ষিত IPC ফ্রেমওয়ার্ক তৈরি করে:

AndroidX Security State Provider লাইব্রেরি, অন-ডিভাইস আপডেট পরিষেবা এবং বিশেষ সুবিধা নেই এমন ক্লায়েন্ট অ্যাপের মধ্যে একটি স্ট্যান্ডার্ডাইজড, সুরক্ষিত IPC ফ্রেমওয়ার্ক স্থাপন করে

ডেটা ডেলিভারি মডেল

ক্লায়েন্ট অ্যাপ, queryAllAvailableUpdates বা fetchAvailableSecurityPatchLevel-এ কল করে আপডেট উপলভ্য আছে কিনা তা কোয়েরি করে। ডিভাইসে, ক্লায়েন্ট লাইব্রেরি অটোমেটিক সব রেজিস্টার করা পরিষেবা খুঁজে পায় এবং সেগুলির সাথে বাইন্ড করে। এই পরিষেবাগুলি সিস্টেম অ্যাপ থেকে UpdateInfoService ক্লাস এক্সটেন্ড করে এবং সেগুলির কাছে READ_PRIVILEGED_PHONE_STATE অনুমতি থাকে।

আগের ডায়াগ্রামে দেখানো হয়েছে যে, security-state-provider লাইব্রেরি দুটি ডেটা ডেলিভারি মডেল কাজ করে:

ডেলিভারি মডেল সিঙ্ক্রোনাইজেশন ট্রিগার ক্লায়েন্টের উত্তর সাজেস্ট করা ব্যবহারিক ক্ষেত্র
পুশ মডেল (ব্যাকগ্রাউন্ড সিঙ্ক) শিডিউল করা ব্যাকগ্রাউন্ড ওয়ার্কার (WorkManager বা JobScheduler) আপনার ব্যাকএন্ডের সাথে সিঙ্ক করে এবং UpdateInfoManager-এ রেকর্ড লেখে। আপনার পরিষেবা সবসময় লোকাল ডিস্ক ক্যাশে (shouldFetchUpdates() = false) থেকে পরিবেশন করে। স্থানীয় ক্যাশে থেকে সঙ্গে সঙ্গে পরিবেশন করা হয়। OEM সিস্টেম OTA আপডেটার এবং ব্যাকগ্রাউন্ডে সিঙ্ক করা মডুলার কম্পোনেন্ট আপডেটার।
পুল মডেল (চাহিদা অনুযায়ী সিঙ্ক করা) ইনকামিং ক্লায়েন্ট IPC কোয়েরি, ক্যাশে করা রেকর্ড পুরনো হয়ে গেলে (shouldFetchUpdates() = true) নেটওয়ার্ক ফেচ ট্রিগার করে। মিউটেক্স কোয়ালিসিং ও রেট লিমিটিং (shouldThrottle()) আপনার ব্যাকএন্ডকে স্পাইক থেকে সুরক্ষিত রাখে। ক্যাশে পুরনো হয়ে গেলে ব্যাকএন্ড থেকে ডেটা ফেচ করার জন্য অপেক্ষা করে। শিডিউল করা ব্যাকগ্রাউন্ড সিঙ্ক ওয়ার্কার ছাড়া মনোলিথিক OEM OTA আপডেটার।

একাধিক আপডেট প্রদানকারী

প্রোডাকশন Android ডিভাইসে, একাধিক স্বতন্ত্র আপডেট প্রদানকারী একসাথে কাজ করে। যেমন, Mainline মডিউলার কম্পোনেন্ট (COMPONENT_SYSTEM_MODULES)-এর উপলভ্যতা প্রকাশ করে, অন্যদিকে আপনার OEM OTA ক্লায়েন্ট প্রাইমারি OS ইমেজ (COMPONENT_SYSTEM)-এর জন্য আপডেট প্রকাশ করে।

আপনার পরিষেবা শুধুমাত্র সেইসব নির্দিষ্ট কম্পোনেন্টের আপডেট রেজিস্টার করে যেগুলি এটি ম্যানেজ করে। কোনও ডিভাইসে একাধিক পরিষেবা প্রদানকারী একই কম্পোনেন্টের জন্য আপডেট প্রকাশ করলে, ক্লায়েন্ট অ্যাপগুলি এন্টারপ্রাইজ অডিটিংয়ের জন্য সর্বোচ্চ উপলভ্য প্যাচ লেভেল (fetchAvailableSecurityPatchLevel() ব্যবহার করে) মূল্যায়ন করে অথবা আলাদা আলাদা UpdateInfo রেকর্ড (queryAllAvailableUpdates() ব্যবহার করে) পরীক্ষা করে। আপনার পরিষেবা যেন সবসময় আপনার কম্পোনেন্টের জন্য ক্যাননিকাল ফর্ম্যাট প্রকাশ করে (DateBasedSecurityPatchLevel COMPONENT_SYSTEM-এর জন্য)।

আপনার আপডেট করা ক্লায়েন্টকে অনবোর্ড করার ধাপে-ধাপে গাইড

আপনার আপডেট ক্লায়েন্টে 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 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 হতে হবে। OTA রিলিজের সময় সহ ব্যবহারকারীদের জন্য আপডেট উপলভ্য করা হলে। পেলোড ডাউনলোড বা ইনস্টল করার সময় ব্যবহার করবেন না।
lastCheckTimeMillis getLastCheckTimeMillis() long Unix epoch থেকে মিলিসেকেন্ডে। > 0 হতে হবে। সিঙ্ক্রোনাইজেশনের সময় আপনার পরিষেবা প্রদানকারী এই আপডেট রেকর্ড যাচাই বা খুঁজে পাওয়ার টাইমস্ট্যাম্প।

নিচের বিকল্পগুলি থেকে আপনার আপডেটারের আর্কিটেকচারের সাথে মানানসই ডেলিভারি মডেল বেছে নিন:

বিকল্প ক: পুশ মডেল (সাজেস্ট করা হয়েছে)

আপনার ব্যাকগ্রাউন্ড সিঙ্ক ওয়ার্কার OTA সার্ভার চেক করার সময়, নিশ্চিত করুন যে কোনও আবিষ্কৃত আপডেট ডিভাইসের বর্তমান প্যাচ লেভেলকে উন্নত করে এবং 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()
    }
}

জাভা

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() ওভাররাইড করে false রিটার্ন করুন, এটি Kotlin-এ UpdateInfoService অথবা Java-তে ListenableFutureUpdateInfoService এক্সটেন্ড করে করুন:

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

জাভা

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

বিকল্প খ: পুল মডেল (অন-ডিমান্ড)

পুল-ভিত্তিক আর্কিটেকচারে, আপনার পরিষেবা অন-ডিমান্ড রিফ্রেশ করার অনুরোধগুলি হ্যান্ডেল করে যা ক্লায়েন্ট অ্যাপগুলি দ্বারা ট্রিগার করা হয় যখন স্থানীয় ক্যাশে পুরানো হয়ে যায়।

চাহিদা মতো আপডেট কোয়েরি ম্যানেজ করতে, Kotlin-এ UpdateInfoService এক্সটেন্ড করুন (সাসপেন্ড করা fetchUpdates() ফাংশন প্রয়োগ করা) অথবা Java-তে ListenableFutureUpdateInfoService (fetchUpdatesAsync() প্রয়োগ করা Guava ListenableFuture রিটার্ন করা):

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

জাভা

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() চালু করা হয়, আপনার আপডেটার 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())

জাভা

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

ধাপ ৫: আপনার ইন্টিগ্রেশন যাচাই করুন

এন্ড-টু-এন্ড ইন্টিগ্রেশন যাচাই করতে এবং সাধারণ OEM ডেপ্লয়মেন্ট সংক্রান্ত সমস্যা এড়াতে, Android Debug Bridge (ADB) ব্যবহার করে Android ডিভাইস বা এমুলেটরে নিম্নলিখিত চেকগুলি চালান:

  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. ব্যবহারকারী ০ ও অফিসের প্রোফাইল, দুটিতেই ইনটেন্ট রেজোলিউশন যাচাই করুন: নিশ্চিত করুন যে Android OS PackageManager আপনার এক্সপোর্ট করা UPDATE_INFO_SERVICE ইনটেন্ট ফিল্টারকে প্রাথমিক ব্যবহারকারী (User 0) ও যেকোনও অ্যাক্টিভ Android Enterprise অফিসের প্রোফাইল (যেমন 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 ওভাররাইড করে 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) অবিলম্বে পরবর্তী কোয়েরিতে লগ করা হয়েছে। (পুল-মডেল টেস্ট রান চলাকালীন ১-ঘণ্টার পারসিস্ট্যান্ট রেট লিমিটার রিসেট করতে, adb shell pm clear <your_package_name> রান করান।)

ঐচ্ছিক ও উন্নত কনফিগারেশন

ক্যাশিং নীতি ও রেট লিমিটিং

ক্লায়েন্ট আপডেট সংক্রান্ত কোয়েরি করলে, UpdateInfoService ব্যাকএন্ড সার্ভারের উপর লোড কমানোর জন্য ডেটা ফ্রেশনেস ব্যালেন্স করতে ডবল-চেক করা লকিং ওয়ার্কফ্লো এক্সিকিউট করে:

ব্যাকএন্ড সার্ভারের লোডের বিপরীতে ডেটা ফ্রেশনেস ব্যালেন্স করতে UpdateInfoService ডাবল-চেক করা লকিং ওয়ার্কফ্লো এক্সিকিউট করে

  • দ্রুত পাথ (shouldFetchUpdates()): ডিফল্ট হিসেবে, গ্লোবাল lastCheckTimeMillis ১ ঘণ্টার বেশি পুরনো হলে (TimeUnit.HOURS.toMillis(1)) তবেই shouldFetchUpdates() true (পুরনো ক্যাশে আছে তা বোঝায়) রিটার্ন করে। shouldFetchUpdates() false রিটার্ন করলে, পরিষেবা অবিলম্বে লক না নিয়ে বা নেটওয়ার্ক I/O না করেই ফলাফল UpdateFetchOutcome.CACHE_HIT সহ ক্যাশে করা রেকর্ড রিটার্ন করে। এই ক্যাশিং নীতি কাস্টমাইজ করতে আপনি shouldFetchUpdates() ওভাররাইড করতে পারবেন।
  • স্লো পাথ ও রিকোয়েস্ট কোয়ালিসিং: shouldFetchUpdates() true রিটার্ন করলে, পরিষেবা একটি ইন্টার্নাল কোরাউটিন মিউটেক্স অ্যাকোয়ার করে এবং shouldFetchUpdates() আবার মূল্যায়ন করে (লকের জন্য অপেক্ষা করার সময় কোনও কনকারেন্ট অনুরোধ ইতিমধ্যেই ক্যাশে রিফ্রেশ করে থাকলে UpdateFetchOutcome.COALESCED রিটার্ন করে)।
  • অবিরাম রেট লিমিটার (shouldThrottle()): কোয়েরি বার্স্ট বা বারবার ব্যর্থতা থেকে ব্যাকএন্ড ইনফ্রাস্ট্রাকচারকে রক্ষা করতে, shouldThrottle() অ্যাপ ও ডিভাইস রিস্টার্ট জুড়ে অবিরাম ন্যূনতম ১-ঘণ্টার ব্যবধান এনফোর্স করে। UpdateInfoService fetchUpdates() ইনভোক করার আগে প্রতিটি প্রচেষ্টা রেকর্ড করে, তাই fetchUpdates() কোনও ব্যতিক্রম ঘটলে (onFetchFailed(e) ইনভোক করার পরে UpdateFetchOutcome.FAILED রিটার্ন করলে), পরবর্তী ৬০ মিনিট ধরে পরবর্তী কোয়েরিগুলি ফলাফল UpdateFetchOutcome.THROTTLED সহ ক্যাশে করা ফলব্যাক ডেটা রিটার্ন করে।

পর্যবেক্ষণযোগ্যতা, টেলিমেট্রি ও ডায়াগনস্টিক

UpdateInfoService ক্লায়েন্ট অ্যাডপশন ট্র্যাক করা, IPC লেটেন্সি মনিটর করা এবং লো-লেভেল AIDL স্টাব ইনস্ট্রুমেন্ট না করেই ব্যাকএন্ড সংক্রান্ত সমস্যার লগ করার জন্য বিল্ট-ইন অবজার্ভেবিলিটি হুক প্রদান করে:

  • 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)
    }
}

জাভা

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 প্রাথমিক বাইন্ডার থ্রেডে (fetchUpdates()-এর আগে Binder.clearCallingIdentity() কল করার আগে) অটোমেটিক getCallerUid() মূল্যায়ন করে, প্যাকেজ মালিকানা যাচাই করে এবং যাচাই করা কলার UID সরাসরি onRequestCompleted(telemetry)-এ onClientConnected(), onClientDisconnected() ও telemetry.callerUid-এ পাস করে।

স্ট্যান্ডার্ড Android <service> কম্পোনেন্টের জন্য, আপনাকে কল করতে বা ওভাররাইড করতে হবে না getCallerUid()। protected open getCallerUid() পদ্ধতি (যা ডিফল্ট হিসেবে Binder.getCallingUid()-এর কাছে ডেলিগেট করে) হোস্ট অ্যাপের জন্য ওভাররাইড হুক হিসেবে প্রদান করা হয় যা ইন্টার্নাল সার্ভিস ব্রোকার বা প্রক্সি আর্কিটেকচারের মাধ্যমে Binder IPC রাউট করে, এর ফলে সাবক্লাস ব্রোকারের UID-এর পরিবর্তে লজিক্যাল ক্লায়েন্ট UID রিটার্ন করতে পারে।

অতিরিক্ত রিসোর্স

প্রকাশনার নিরাপত্তা সংক্রান্ত অবস্থা সম্পর্কে আরও জানতে, নিম্নলিখিত রিসোর্স দেখুন:

ডকুমেন্টেশন

API রেফারেন্স