หากคุณเป็นผู้ผลิตอุปกรณ์ดั้งเดิม (OEM) หรือดูแลไคลเอ็นต์การอัปเดตแบบ Over-The-Air (OTA) ที่มีสิทธิ์ คุณสามารถให้แอปที่มีความละเอียดอ่อนด้านความปลอดภัยในอุปกรณ์มองเห็นการอัปเดตความปลอดภัยที่รอดำเนินการ เพื่อให้แอปประเมินท่าทางด้านความปลอดภัยของอุปกรณ์ได้อย่างถูกต้อง หากต้องการบังคับใช้นโยบายแบบ Zero Trust ที่มีประสิทธิภาพ แอปต้องตรวจสอบได้ไม่เพียงแค่ระดับแพตช์ที่ติดตั้งในอุปกรณ์ (ระดับแพตช์ด้านความปลอดภัยของอุปกรณ์หรือ DSPL) แต่ยังต้องตรวจสอบได้ว่ามีการอัปเดตความปลอดภัยใดบ้าง ที่พร้อมใช้งานและพร้อมติดตั้ง (ระดับแพตช์ด้านความปลอดภัยที่พร้อมใช้งานหรือ ASPL)
เนื่องจากแอปไคลเอ็นต์ที่ไม่มีสิทธิ์อ่านพร็อพเพอร์ตี้ของเฟิร์มแวร์โดยตรง ตรวจสอบฐานข้อมูลโปรแกรมอัปเดตส่วนตัว หรือค้นหาข้อมูลปลายทางของแบ็กเอนด์ OEM ภายในไม่ได้ ไลบรารี AndroidX Security State Provider จึงมีสถาปัตยกรรมการสื่อสารระหว่างโปรเซส (IPC) ที่ได้มาตรฐานและปลอดภัย ซึ่งไคลเอ็นต์การอัปเดตใช้เพื่อแชร์ข้อมูลเกี่ยวกับการอัปเดตที่พร้อมใช้งาน การใช้
UpdateInfoService ในไคลเอ็นต์การอัปเดต OTA จะช่วยให้คุณเผยแพร่ข้อมูลเมตา ASPL
สำหรับระบบได้โดยไม่ต้องเปิดเผยการผสานรวมแบ็กเอนด์ที่เป็นกรรมสิทธิ์ แม้ว่า Google จะมีการติดตั้งใช้งานสำหรับโปรแกรมอัปเดตคอมโพเนนต์ของระบบแบบแยกส่วน (Mainline)
สำหรับอุปกรณ์ GMS แต่อุปกรณ์ที่ไม่ใช่ GMS ก็สามารถเผยแพร่ข้อมูลเมตา ASPL สำหรับ
คอมโพเนนต์ของระบบแบบแยกส่วนเหล่านี้ได้เช่นกัน
ภาพรวมสถาปัตยกรรม
แผนภาพต่อไปนี้แสดงให้เห็นว่าไลบรารี AndroidX Security State Provider สร้างเฟรมเวิร์ก IPC ที่ได้มาตรฐานและปลอดภัยระหว่างแอปไคลเอ็นต์ที่ไม่มีสิทธิ์ และบริการอัปเดตในอุปกรณ์ได้อย่างไร

รูปแบบการนำส่งข้อมูล
แอปไคลเอ็นต์จะค้นหาการอัปเดตที่พร้อมใช้งานได้โดยการเรียกใช้
queryAllAvailableUpdates หรือ
fetchAvailableSecurityPatchLevel ไลบรารี
ไคลเอ็นต์จะค้นหาและเชื่อมโยงกับบริการที่ลงทะเบียนทั้งหมดซึ่งขยายคลาส UpdateInfoService ในอุปกรณ์จากแอปของระบบที่มีสิทธิ์ READ_PRIVILEGED_PHONE_STATE โดยอัตโนมัติ
ดังที่แสดงในแผนภาพก่อนหน้า ไลบรารี security-state-provider
รองรับรูปแบบการนำส่งข้อมูล 2 รูปแบบ ได้แก่
| รูปแบบการนำส่ง | ทริกเกอร์การซิงค์ | การตอบกลับของไคลเอ็นต์ | กรณีการใช้งานที่แนะนำ |
|---|---|---|---|
| โมเดลพุช (การซิงค์ในเบื้องหลัง) | Worker ที่ทำงานในเบื้องหลังตามกำหนดเวลา (WorkManager หรือ JobScheduler) จะซิงค์กับแบ็กเอนด์และเขียนบันทึกลงใน UpdateInfoManager บริการจะแสดงจากแคชดิสก์ในเครื่องเสมอ (shouldFetchUpdates() = false) |
แสดงจากแคชในเครื่องทันที | โปรแกรมอัปเดต OTA ของระบบ OEM และโปรแกรมอัปเดตคอมโพเนนต์แบบโมดูลาร์ที่ซิงค์ในเบื้องหลัง |
| ดึงโมเดล (ซิงค์ตามต้องการ) | การค้นหา IPC ของไคลเอ็นต์ขาเข้าจะทริกเกอร์การดึงข้อมูลเครือข่ายเมื่อระเบียนที่แคชไว้ล้าสมัย (shouldFetchUpdates() = true) การรวม Mutex และการจำกัดอัตรา (shouldThrottle()) จะปกป้องแบ็กเอนด์ของคุณจากการเพิ่มขึ้นอย่างรวดเร็ว |
รอการดึงข้อมูลจากแบ็กเอนด์เมื่อแคชล้าสมัย | โปรแกรมอัปเดต OTA ของ OEM แบบ Monolithic ที่ไม่มี Worker การซิงค์ในเบื้องหลังที่กำหนดเวลาไว้ |
ผู้ให้บริการอัปเดตหลายราย
ในอุปกรณ์ Android ที่ใช้งานจริง ผู้ให้บริการอัปเดตอิสระหลายรายจะทำงานร่วมกัน
พร้อมกัน เช่น Mainline จะเผยแพร่ความพร้อมใช้งานสำหรับคอมโพเนนต์แบบโมดูล (COMPONENT_SYSTEM_MODULES) ในขณะที่ไคลเอ็นต์ OTA ของ OEM จะเผยแพร่การอัปเดตสำหรับอิมเมจระบบปฏิบัติการหลัก (COMPONENT_SYSTEM)
บริการของคุณจะต้องลงทะเบียนการอัปเดตสำหรับคอมโพเนนต์ที่เฉพาะเจาะจงที่บริการจัดการเท่านั้น หากผู้ให้บริการหลายรายในอุปกรณ์เผยแพร่การอัปเดตสำหรับคอมโพเนนต์เดียวกัน แอปไคลเอ็นต์จะประเมินระดับแพตช์สูงสุดที่มี (โดยใช้ fetchAvailableSecurityPatchLevel()) หรือตรวจสอบบันทึก UpdateInfo แต่ละรายการ (โดยใช้ queryAllAvailableUpdates()) เพื่อการตรวจสอบระดับองค์กร ตรวจสอบว่าบริการของคุณเผยแพร่รูปแบบ Canonical สำหรับคอมโพเนนต์ (DateBasedSecurityPatchLevel สำหรับ
COMPONENT_SYSTEM) เสมอ
คำแนะนำทีละขั้นตอนในการเริ่มต้นใช้งานไคลเอ็นต์การอัปเดต
ทำตามขั้นตอนต่อไปนี้เพื่อผสานรวมไลบรารี AndroidX Security State Provider เข้ากับไคลเอ็นต์การอัปเดตและเริ่มเผยแพร่ความพร้อมใช้งาน การอัปเดตความปลอดภัยของอุปกรณ์
ขั้นตอนที่ 1: เพิ่มการอ้างอิง
หากต้องการใช้ผู้ให้บริการอัปเดต โปรดตรวจสอบว่าโปรเจ็กต์มีที่เก็บ Maven ของ Google จากนั้นเพิ่มไลบรารี security-state-provider ลงในไฟล์ build.gradle.kts (Kotlin DSL) หรือ build.gradle (Groovy DSL) ของโมดูล
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'
}
ขั้นตอนที่ 2: ประกาศบริการอัปเดตในไฟล์ Manifest
ประกาศบริการใน 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 ให้ประกาศสิทธิ์ต่อไปนี้ในไฟล์ Manifest และตรวจสอบว่าได้เพิ่มสิทธิ์เหล่านั้นลงในรายการที่อนุญาตสำหรับสิทธิ์
ที่มีสิทธิ์เข้าถึงระดับสูงแล้ว
READ_PRIVILEGED_PHONE_STATE: จำเป็นเพื่อให้ลูกค้าเชื่อมั่นในผู้ให้บริการของคุณINTERACT_ACROSS_USERS: ต้องระบุสำหรับandroid:singleUser="true"
ขั้นตอนที่ 3: ติดตั้งใช้งาน UpdateInfoService
หากต้องการเผยแพร่สถานะการอัปเดต คุณต้องใช้คลาส UpdateInfoService
และสร้างบันทึกการอัปเดตที่ตรงกับโมเดลข้อมูลที่คาดไว้
ข้อกำหนดของโมเดลข้อมูล UpdateInfo
ไม่ว่าคุณจะเลือกรูปแบบ Push หรือ Pull ให้สร้างระเบียน UpdateInfo
โดยใช้ UpdateInfo.Builder ตามข้อกำหนดต่อไปนี้
| ชื่อฟิลด์ | เมธอด Getter | ประเภทข้อมูล | ข้อกำหนดด้านการตรวจสอบและความถูกต้องของรูปแบบ | จุดประสงค์และความหมายของระบบ |
|---|---|---|---|---|
component |
getComponent() |
String (@Component) |
ค่าคงที่ Canonical ใน 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 |
การประทับเวลาเมื่อผู้ให้บริการยืนยันหรือค้นพบระเบียนการอัปเดตนี้ระหว่างการซิงค์ |
เลือกรุ่นการนำส่งที่เหมาะกับสถาปัตยกรรมของโปรแกรมอัปเดตจากตัวเลือกต่อไปนี้
ตัวเลือก A: โมเดลพุช (แนะนำ)
เมื่อ Worker การซิงค์ข้อมูลในเบื้องหลังตรวจสอบเซิร์ฟเวอร์ OTA ให้ตรวจสอบว่าการอัปเดตที่พบจะเลื่อนระดับแพตช์ปัจจุบันของอุปกรณ์และคงไว้โดยใช้ UpdateInfoManager.registerUpdate() หรือเรียกใช้ UpdateInfoManager.unregisterUpdate() หากไม่มีการอัปเดตความปลอดภัยที่รอดำเนินการ เรียกใช้ UpdateInfoManager.setLastCheckTimeMillis() ที่ส่วนท้ายของการซิงค์ทุกครั้งเสมอ (แม้หลังจากเรียกใช้ registerUpdate() ซึ่งจะคงบันทึก UpdateInfo ต่อคอมโพเนนต์ไว้ แต่จะไม่
อัปเดตการประทับเวลาการตรวจสอบล่าสุดทั่วโลกที่ส่งคืนให้ไคลเอ็นต์) คุณสามารถติดตั้งใช้งานได้ด้วย
WorkManager CoroutineWorker ใน Kotlin หรือ Worker ใน Java
Kotlin
import android.content.Context
import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlin.math.max
class OtaSyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val updateInfoManager = UpdateInfoManager(applicationContext)
val securityPatchState = SecurityPatchState(applicationContext)
val currentSpl = securityPatchState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
// 1. Fetch available update metadata from OEM backend
val latestUpdate = MyOtaClient.fetchLatestSystemUpdate()
val targetSplString = latestUpdate?.spl?.trim()
val targetSpl = if (!targetSplString.isNullOrEmpty()) {
DateBasedSecurityPatchLevel.fromString(targetSplString)
} else {
null
}
// 2. Defensively verify that target SPL is non-blank AND strictly newer than installed DSPL.
// If an update is a maintenance patch with no SPL increment (or if no update is available),
// unregister any stale cached record for this component.
if (latestUpdate != null && targetSpl != null && targetSpl > currentSpl) {
val updateInfo = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(targetSpl)
.setPublishedDateMillis(latestUpdate.releaseTimeMillis)
.setLastCheckTimeMillis(System.currentTimeMillis())
.build()
updateInfoManager.registerUpdate(updateInfo)
} else {
val clearTarget = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
updateInfoManager.unregisterUpdate(clearTarget)
}
// 3. Update global freshness timestamp (monotonic synchronization)
val currentCheckTime = System.currentTimeMillis()
val previousCheckTime = updateInfoManager.getLastCheckTimeMillis()
updateInfoManager.setLastCheckTimeMillis(max(previousCheckTime, currentCheckTime))
return Result.success()
}
}
Java
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.UpdateInfoManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class OtaSyncWorker extends Worker {
public OtaSyncWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
// In Java, pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
UpdateInfoManager updateInfoManager =
new UpdateInfoManager(getApplicationContext(), /* customSecurityState= */ null);
SecurityPatchState securityPatchState = new SecurityPatchState(getApplicationContext());
SecurityPatchLevel currentSpl =
securityPatchState.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);
// 1. Fetch available update metadata from OEM backend
MyOtaUpdate latestUpdate = MyOtaClient.fetchLatestSystemUpdate();
String targetSplString = (latestUpdate != null && latestUpdate.getSpl() != null)
? latestUpdate.getSpl().trim()
: null;
DateBasedSecurityPatchLevel targetSpl =
!TextUtils.isEmpty(targetSplString)
? DateBasedSecurityPatchLevel.fromString(targetSplString)
: null;
// 2. Defensively verify that target SPL is non-blank AND strictly newer than installed DSPL.
// If an update is a maintenance patch with no SPL increment (or if no update is available),
// unregister any stale cached record for this component.
if (latestUpdate != null && targetSpl != null && targetSpl.compareTo(currentSpl) > 0) {
UpdateInfo updateInfo = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(targetSpl)
.setPublishedDateMillis(latestUpdate.getReleaseTimeMillis())
.setLastCheckTimeMillis(System.currentTimeMillis())
.build();
updateInfoManager.registerUpdate(updateInfo);
} else {
UpdateInfo clearTarget = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
updateInfoManager.unregisterUpdate(clearTarget);
}
// 3. Update global freshness timestamp (monotonic synchronization)
long currentCheckTime = System.currentTimeMillis();
long previousCheckTime = updateInfoManager.getLastCheckTimeMillis();
updateInfoManager.setLastCheckTimeMillis(Math.max(previousCheckTime, currentCheckTime));
return Result.success();
}
}
ในโมเดลแบบพุช งานในเบื้องหลังจะบันทึกการอัปเดตโดยตรงไปยัง
UpdateInfoManager หากต้องการสั่งให้เฟรมเวิร์กแสดงบันทึกจากที่เก็บข้อมูลในดิสก์ภายในเสมอ ให้ลบล้าง shouldFetchUpdates() เพื่อแสดง false โดย
ขยาย UpdateInfoService ใน Kotlin หรือ
ListenableFutureUpdateInfoService ใน Java
Kotlin
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoService
class PushUpdateInfoService : UpdateInfoService() {
// Cache is populated out-of-band by background sync tasks
override fun shouldFetchUpdates(): Boolean = false
// Never invoked under normal flow because shouldFetchUpdates() returns false
override suspend fun fetchUpdates(): List<UpdateInfo> = emptyList()
}
Java
import androidx.annotation.NonNull;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collections;
import java.util.List;
public class PushUpdateInfoService extends ListenableFutureUpdateInfoService {
@Override
protected boolean shouldFetchUpdates() {
return false;
}
@NonNull
@Override
protected ListenableFuture<List<UpdateInfo>> fetchUpdatesAsync() {
return Futures.immediateFuture(Collections.emptyList());
}
}
ตัวเลือก ข: โมเดลแบบดึง (ตามต้องการ)
ในสถาปัตยกรรมแบบดึง บริการของคุณจะจัดการคำขอรีเฟรชตามต้องการ ที่แอปไคลเอ็นต์ทริกเกอร์เมื่อแคชในเครื่องล้าสมัย
หากต้องการจัดการการค้นหาการอัปเดตตามคำขอ ให้ขยาย UpdateInfoService ใน Kotlin
(การใช้ฟังก์ชัน fetchUpdates() ที่ระงับ) หรือ
ListenableFutureUpdateInfoService ใน Java (การใช้
fetchUpdatesAsync() ที่ส่งคืน ListenableFuture ของ Guava)
Kotlin
package com.example.android.updater
import androidx.security.state.SecurityPatchState
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel
import androidx.security.state.UpdateInfo
import androidx.security.state.provider.UpdateInfoManager
import androidx.security.state.provider.UpdateInfoService
import java.util.concurrent.TimeUnit
class MyUpdateInfoService : UpdateInfoService() {
// Manage local update records and check timestamps
private val updateInfoManager by lazy { UpdateInfoManager(this) }
override suspend fun fetchUpdates(): List<UpdateInfo> {
val currentSpl = SecurityPatchState(this)
.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM)
// 1. Execute network request to OTA backend
val response = MyOtaBackendClient.checkAvailableUpdates()
// 2. Defensively filter out blank or non-advancing SPLs and map to UpdateInfo objects
val validUpdates = response.updates
.mapNotNull { updateItem ->
val splString = updateItem.targetSpl?.trim()
if (splString.isNullOrEmpty()) return@mapNotNull null
val parsedSpl = DateBasedSecurityPatchLevel.fromString(splString)
if (parsedSpl > currentSpl) {
UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(parsedSpl)
.setPublishedDateMillis(updateItem.releaseTimestampMillis)
.setLastCheckTimeMillis(System.currentTimeMillis())
.build()
} else {
null
}
}
// 3. If no advancing SYSTEM update is available (or if a previously offered update was revoked),
// proactively unregister any cached record for this component.
if (validUpdates.isEmpty()) {
val clearTarget = UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build()
updateInfoManager.unregisterUpdate(clearTarget)
}
return validUpdates
}
override fun shouldFetchUpdates(): Boolean {
// Enforce custom freshness threshold (for example, 4 hours instead of default 1 hour)
val lastCheckMillis = updateInfoManager.getLastCheckTimeMillis()
val dataAge = System.currentTimeMillis() - lastCheckMillis
return dataAge > TimeUnit.HOURS.toMillis(4)
}
}
Java
package com.example.android.updater;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.security.state.SecurityPatchState;
import androidx.security.state.SecurityPatchState.DateBasedSecurityPatchLevel;
import androidx.security.state.SecurityPatchState.SecurityPatchLevel;
import androidx.security.state.UpdateInfo;
import androidx.security.state.provider.ListenableFutureUpdateInfoService;
import androidx.security.state.provider.UpdateInfoManager;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class MyUpdateInfoService extends ListenableFutureUpdateInfoService {
private UpdateInfoManager updateInfoManager;
@Override
public void onCreate() {
super.onCreate();
// Pass null for customSecurityState because UpdateInfoManager does not declare @JvmOverloads
updateInfoManager = new UpdateInfoManager(this, /* customSecurityState= */ null);
}
@NonNull
@Override
protected ListenableFuture<List<UpdateInfo>> fetchUpdatesAsync() {
try {
SecurityPatchLevel currentSpl = new SecurityPatchState(this)
.getDeviceSecurityPatchLevel(SecurityPatchState.COMPONENT_SYSTEM);
MyOtaBackendResponse response = MyOtaBackendClient.checkAvailableUpdates();
List<UpdateInfo> updates = new ArrayList<>();
for (MyOtaUpdateItem item : response.getUpdates()) {
String trimmedSpl = (item.getTargetSpl() != null) ? item.getTargetSpl().trim() : null;
if (!TextUtils.isEmpty(trimmedSpl)) {
DateBasedSecurityPatchLevel parsedSpl =
DateBasedSecurityPatchLevel.fromString(trimmedSpl);
if (parsedSpl.compareTo(currentSpl) > 0) {
updates.add(new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.setSecurityPatchLevel(parsedSpl)
.setPublishedDateMillis(item.getReleaseTimestampMillis())
.setLastCheckTimeMillis(System.currentTimeMillis())
.build());
}
}
}
// If no advancing SYSTEM update is available (or if a previously offered update was revoked),
// proactively unregister any cached record for this component.
if (updates.isEmpty()) {
UpdateInfo clearTarget = new UpdateInfo.Builder()
.setComponent(SecurityPatchState.COMPONENT_SYSTEM)
.build();
updateInfoManager.unregisterUpdate(clearTarget);
}
return Futures.immediateFuture(updates);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
}
@Override
protected boolean shouldFetchUpdates() {
long lastCheckMillis = updateInfoManager.getLastCheckTimeMillis();
long dataAge = System.currentTimeMillis() - lastCheckMillis;
return dataAge > TimeUnit.HOURS.toMillis(4);
}
}
ขั้นตอนที่ 4: ล้างการอัปเดตที่ใช้หลังจากรีบูตอุปกรณ์
แม้ว่า UpdateInfoManager จะตัดการอัปเดตที่ล้าสมัยออกโดยอัตโนมัติทุกครั้งที่เรียกใช้ registerUpdate() แต่โปรแกรมอัปเดตจะไม่เรียกใช้ registerUpdate() อีกครั้งหลังจากที่การอัปเดต OTA ติดตั้งเสร็จจนกว่าจะถึงรอบการซิงค์เซิร์ฟเวอร์ที่กำหนดเวลาไว้ครั้งถัดไป หากต้องการป้องกันไม่ให้แอปไคลเอ็นต์เห็นการอัปเดตที่ติดตั้งแล้วว่ายังรอดำเนินการอยู่ทันทีหลังจากรีบูต ให้รอรับ ACTION_BOOT_COMPLETED และเรียกใช้ UpdateInfoManager.unregisterUpdate() เมื่อการอัปเดต OTA ติดตั้งเสร็จแล้วเพื่อล้างบันทึกจากแคชในเครื่อง การดำเนินการนี้เฉพาะเมื่อการติดตั้ง
อัปเดตเสร็จสิ้นแล้วจะช่วยหลีกเลี่ยงการล้างข้อมูลการอัปเดตที่รอดำเนินการ
(ที่ถอนการติดตั้งแล้ว) โดยไม่มีเงื่อนไขในการรีบูตอุปกรณ์ตามปกติทุกครั้ง เนื่องจากคีย์
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 หรือโปรแกรมจำลองโดยใช้ Android Debug Bridge (ADB) เพื่อตรวจสอบการผสานรวมตั้งแต่ต้นทางถึงปลายทางและหลีกเลี่ยงข้อผิดพลาดในการติดตั้งใช้งาน OEM ทั่วไป
ยืนยันว่าไคลเอ็นต์เชื่อถือผู้ให้บริการ: แอปไคลเอ็นต์จะไม่สนใจผู้ให้บริการที่ไม่มี
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หากลูกค้ายังคงไม่เห็นผู้ให้บริการ ให้ตรวจสอบ logcat สำหรับ
Ignoring untrusted update providerจากแท็กSecurityPatchStateยืนยันการแก้ปัญหา Intent ในทั้งโปรไฟล์ผู้ใช้ 0 และโปรไฟล์งาน: ยืนยันว่า ระบบปฏิบัติการ Android
PackageManagerแก้ปัญหาตัวกรอง Intent ที่ส่งออก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ยืนยันสถานะบริการและบันทึกที่แคชไว้โดยใช้
dumpsys:UpdateInfoServiceจะลบล้างdump()เพื่อรายงานGlobal Last Check,Should Throttle(สถานะของเครื่องมือจำกัดอัตรา) และCached Updatesเนื่องจากUpdateInfoServiceเป็นบริการที่เชื่อมโยงและ ไคลเอ็นต์จะยกเลิกการเชื่อมโยงทันทีหลังจากทำการค้นหาdumpsys activity serviceจะแสดงผล(nothing)เมื่อไม่มีไคลเอ็นต์ที่เชื่อมโยง เริ่มบริการอย่างชัดเจนก่อน เรียกใช้dumpsysadb 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เรียกใช้การเชื่อมโยงไคลเอ็นต์และยืนยันผลลัพธ์การวัดและส่งข้อมูล: จากแอปทดสอบที่ไม่มีสิทธิ์ (ไม่มีสิทธิ์ลายเซ็นของระบบ) ให้เรียกใช้
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) ใน การค้นหาที่ตามมาทันที (หากต้องการรีเซ็ตตัวจำกัดอัตราแบบต่อเนื่อง 1 ชั่วโมง ระหว่างการทดสอบแบบพุล ให้เรียกใช้adb shell pm clear <your_package_name>)
- ยืนยันว่าไคลเอ็นต์ที่ไม่มีสิทธิ์เชื่อมโยงโดยไม่มี
การกำหนดค่าขั้นสูงและที่ไม่บังคับ
นโยบายการแคชและการจำกัดอัตราคำขอ
เมื่อไคลเอ็นต์ค้นหาการอัปเดต UpdateInfoService จะเรียกใช้เวิร์กโฟลว์การล็อกที่ตรวจสอบ 2 ครั้งเพื่อปรับสมดุลความใหม่ของข้อมูลกับภาระงานของเซิร์ฟเวอร์แบ็กเอนด์

- Fast Path (
shouldFetchUpdates()): โดยค่าเริ่มต้นshouldFetchUpdates()จะแสดงผลtrue(ระบุแคชที่ล้าสมัย) เฉพาะ เมื่อlastCheckTimeMillisทั่วโลกมีอายุมากกว่า 1 ชั่วโมง (TimeUnit.HOURS.toMillis(1)) เมื่อshouldFetchUpdates()แสดงผลfalseบริการจะแสดงผลระเบียนที่แคชไว้ทันทีพร้อมผลลัพธ์UpdateFetchOutcome.CACHE_HITโดยไม่ต้องรับล็อกหรือดำเนินการ I/O เครือข่าย คุณสามารถลบล้างshouldFetchUpdates()เพื่อปรับแต่งนโยบายการแคชนี้ได้ - เส้นทางช้าและการรวมคำขอ: เมื่อ
shouldFetchUpdates()แสดงผลtrueบริการจะรับ Mutex ของโครูทีนภายในและประเมินshouldFetchUpdates()อีกครั้ง (แสดงผลUpdateFetchOutcome.COALESCEDหากคำขอหลายรายการพร้อมกันรีเฟรชแคชแล้วขณะรอล็อก) - เครื่องมือจำกัดอัตราแบบถาวร (
shouldThrottle()): เพื่อปกป้องโครงสร้างพื้นฐานของแบ็กเอนด์ จากการค้นหาที่เพิ่มขึ้นอย่างรวดเร็วหรือความล้มเหลวซ้ำๆshouldThrottle()จะบังคับใช้ช่วงเวลาขั้นต่ำแบบถาวร 1 ชั่วโมงในการรีสตาร์ทแอปและอุปกรณ์UpdateInfoServiceจะบันทึกความพยายามแต่ละครั้งก่อนเรียกใช้fetchUpdates()ดังนั้นหากfetchUpdates()แสดงข้อยกเว้น (ส่งคืนUpdateFetchOutcome.FAILEDหลังจากเรียกใช้onFetchFailed(e)) การค้นหาต่อๆ ไปในช่วง 60 นาทีถัดไปจะส่งคืนข้อมูลสำรองที่แคชไว้พร้อมผลลัพธ์UpdateFetchOutcome.THROTTLEDอย่างราบรื่น
ความสามารถในการสังเกต การวัดและส่งข้อมูลทางไกล และการวินิจฉัย
UpdateInfoService มีฮุกการสังเกตการณ์ในตัวเพื่อติดตามการนำไคลเอ็นต์ไปใช้
ตรวจสอบเวลาในการตอบสนองของ IPC และบันทึกข้อผิดพลาดของแบ็กเอนด์โดยไม่ต้องใช้เครื่องมือ
Stub AIDL ระดับต่ำ
onRequestCompleted(telemetry): เรียกใช้เมื่อการตรวจสอบการอัปเดตแต่ละครั้งเสร็จสมบูรณ์พร้อมสรุปUpdateCheckTelemetryonClientConnected(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()) และรายงานผลลัพธ์ 1 ใน 5 รายการที่กำหนดไว้
ใน UpdateFetchOutcome
| ค่าคงที่ของผลลัพธ์ | @IntDef รหัส |
พร็อพเพอร์ตี้ของเมตริกที่บันทึก | คำอธิบายและสถานะระบบ |
|---|---|---|---|
UpdateFetchOutcome.CACHE_HIT |
1 |
totalDurationMillis, processingDurationMillis, callerUid |
แสดงจากแคชหน่วยความจำ/ดิสก์ในเครื่องทันทีใน Fast Path (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) ส่งคืนการสำรองข้อมูลที่แคชไว้ |
Hook โบรกเกอร์บริการขั้นสูง: getCallerUid()
เมื่อไคลเอ็นต์เชื่อมต่อ UpdateInfoService จะประเมิน getCallerUid() โดยอัตโนมัติในเธรด Binder เริ่มต้น (ก่อนเรียกใช้ Binder.clearCallingIdentity() ก่อน fetchUpdates()) ยืนยันการเป็นเจ้าของแพ็กเกจ และส่ง UID ของผู้โทรที่ยืนยันแล้วไปยัง onClientConnected(), onClientDisconnected() และ telemetry.callerUid โดยตรงใน onRequestCompleted(telemetry)
สําหรับ<service>คอมโพเนนต์getCallerUid() Android มาตรฐาน คุณไม่จําเป็นต้องเรียกใช้หรือลบล้าง เมธอด protected open getCallerUid() (ซึ่งมอบสิทธิ์ให้ Binder.getCallingUid() โดยค่าเริ่มต้น) มีไว้เป็นฮุกการลบล้างสำหรับแอปโฮสต์ที่กำหนดเส้นทาง Binder IPC ผ่านโบรกเกอร์บริการภายในหรือสถาปัตยกรรมพร็อกซี ซึ่งช่วยให้คลาสย่อยแสดงผล UID ของไคลเอ็นต์เชิงตรรกะแทน UID ของโบรกเกอร์ได้
แหล่งข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับการเผยแพร่สถานะความปลอดภัยได้ที่แหล่งข้อมูลต่อไปนี้
เอกสารประกอบ
- ทำความเข้าใจสถานะความปลอดภัยของอุปกรณ์
- ประกาศข่าวสารด้านความปลอดภัยของ Android
- คอมโพเนนต์ของระบบแบบแยกส่วน
- แพตช์ด้านความปลอดภัยเพิ่มเติม
- บันทึกประจำรุ่น Security State Provider 1.0.0
การอ้างอิง API
UpdateInfoServiceListenableFutureUpdateInfoServiceUpdateInfoManagerUpdateInfoUpdateInfo.BuilderUpdateCheckTelemetryUpdateFetchOutcomeSecurityPatchState