Android 17 為開發人員推出了強大的新功能和 API。以下各節將摘要說明這些功能,協助您開始使用相關 API。
如需新增、修改及移除 API 的詳細清單,請參閱 API 差異比較表。如要進一步瞭解新的 API,請參閱 Android API 參考資料 - 新的 API 會醒目顯示,以利於查看。
此外,您也應查看平台變更可能對應用程式造成的影響。詳情請參閱下列頁面:
核心功能
Android 17 新增下列與 Android 核心功能相關的新功能。
新的 ProfilingManager 觸發條件
Android 17 在 ProfilingManager 中新增多個系統觸發條件,協助您收集深入資料,偵錯效能問題。
新觸發條件如下:
TRIGGER_TYPE_COLD_START:在應用程式冷啟動期間觸發。並在回應中提供呼叫堆疊樣本和系統追蹤記錄。TRIGGER_TYPE_OOM:當應用程式擲回OutOfMemoryError時,就會觸發這項功能,並提供 Java 記憶體快照資料做為回應。TRIGGER_TYPE_KILL_EXCESSIVE_CPU_USAGE:當應用程式因 CPU 使用率異常過高而終止時,就會觸發這項事件,並提供呼叫堆疊樣本做為回應。TRIGGER_TYPE_ANOMALY:偵測系統效能異常,例如過多繫結器呼叫和過多記憶體用量。
如要瞭解如何設定系統觸發條件,請參閱觸發條件式剖析和擷取及分析剖析資料的說明文件。
應用程式異常狀況的分析觸發條件
Android 17 推出裝置端異常偵測服務,可監控耗用大量資源的行為,以及潛在的相容性迴歸問題。這項服務與 ProfilingManager 整合,可讓應用程式接收由系統偵測到的特定事件觸發的分析構件。
使用 TRIGGER_TYPE_ANOMALY 觸發條件偵測系統效能問題,例如過多的繫結器呼叫和過多的記憶體用量。當應用程式超出 OS 定義的記憶體限制時,異常狀況觸發條件可讓開發人員接收應用程式專屬的記憶體快照資料,協助找出並修正記憶體問題。此外,如果 Binder 垃圾訊息過多,異常狀況觸發條件會提供 Binder 交易的堆疊取樣快照。
這項 API 回呼會在系統強制執行任何措施前發生。舉例來說,這項功能可協助開發人員在應用程式因超出記憶體限制而遭系統終止前,收集偵錯資料。
val profilingManager =
applicationContext.getSystemService(ProfilingManager::class.java)
val triggers = ArrayList<ProfilingTrigger>()
triggers.add(ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANOMALY))
val mainExecutor: Executor = Executors.newSingleThreadExecutor()
val resultCallback = Consumer<ProfilingResult> { profilingResult ->
if (profilingResult.errorCode != ProfilingResult.ERROR_NONE) {
// upload profile result to server for further analysis
setupProfileUploadWorker(profilingResult.resultFilePath)
}
profilingManager.registerForAllProfilingResults(mainExecutor,
resultCallback)
profilingManager.addProfilingTriggers(triggers)
}
JobDebugInfo API
Android 17 推出新的 JobDebugInfo API,協助開發人員對 JobScheduler 工作進行偵錯,例如工作未執行的原因、執行時間長度,以及其他匯總資訊。
擴充 JobDebugInfo API 的第一個方法是 getPendingJobReasonStats(),會傳回工作處於待執行狀態的原因,以及各自的累計待處理時間。這個方法結合了 getPendingJobReasonsHistory() 和 getPendingJobReasons() 方法,可讓您深入瞭解排定的工作為何未按預期執行,但透過單一方法提供時間長度和工作原因,簡化資訊擷取作業。
舉例來說,對於指定的 jobId,方法可能會傳回 PENDING_JOB_REASON_CONSTRAINT_CHARGING 和 60000 毫秒的時間長度,表示工作因不符合充電限制而待處理 60000 毫秒。
透過監聽器支援閒置時允許的鬧鐘,減少 Wake Lock
Android 17 推出新版 AlarmManager.setExactAndAllowWhileIdle,可接受 OnAlarmListener,而非 PendingIntent。這項新的回呼機制非常適合目前依賴持續喚醒鎖定執行週期性工作的應用程式,例如維護通訊端連線的訊息應用程式。
隱私權
Android 17 包含下列新功能,可提升使用者隱私。
Encrypted Client Hello (ECH) 平台支援
Android 17 推出平台支援的 Encrypted Client Hello (ECH),可大幅提升網路通訊的隱私權。ECH 是 TLS 1.3 擴充功能,可在初始 TLS 握手期間加密伺服器名稱指標 (SNI)。這項加密功能可讓網路中介者更難以識別應用程式連線的特定網域,有助於保護使用者隱私。
平台現在包含網路程式庫實作 ECH 所需的 API。這包括 DnsResolver 中的新功能,可查詢含有 ECH 設定的 HTTPS DNS 記錄,以及 Conscrypt 的 SSLEngines 和 SSLSockets 中的新方法,可在連線至網域時傳遞這些設定來啟用 ECH。開發人員可以透過網路安全性設定檔中的新 <domainEncryption> 元素,設定 ECH 偏好設定,例如視情況啟用或強制使用 ECH,適用於全域或個別網域。
HttpEngine、WebView 和 OkHttp 等熱門網路程式庫預計會在日後的更新中整合這些平台 API,讓應用程式更容易採用 ECH 並提升使用者隱私權。
詳情請參閱「加密用戶端問候訊息」說明文件。
Android 聯絡人選擇工具
Android 聯絡人選擇工具是標準化的可瀏覽介面,可讓使用者與應用程式分享聯絡人。這項工具適用於搭載 Android 17 (API 級別 37) 以上版本的裝置,可做為廣泛 READ_CONTACTS 權限的替代方案,保護使用者隱私。應用程式不會要求存取使用者的整個通訊錄,而是指定所需的資料欄位 (例如電話號碼或電子郵件地址),並由使用者選取要分享的特定聯絡人。這會授予應用程式對所選資料的讀取權限,確保精細控管,同時提供一致的使用者體驗,包括內建搜尋、設定檔切換和多重選取功能,無須建構或維護 UI。
詳情請參閱聯絡人選擇工具說明文件。
安全性
Android 17 新增下列功能,可提升裝置和應用程式的安全性。
Android 進階保護模式 (AAPM)
Android 進階保護模式為 Android 使用者提供一系列強大的全新安全防護功能,在保護使用者 (尤其是高風險使用者) 免於遭受複雜攻擊方面,邁出重要的一步。AAPM 是一項可選擇啟用的功能,只要設定一次即可啟用,使用者隨時都能開啟這項功能,套用一組預設的安全防護措施。
這些核心設定包括禁止從不明來源安裝應用程式 (側載)、限制 USB 資料訊號,以及強制執行 Google Play 安全防護掃描,大幅縮減裝置的攻擊面。開發人員可以透過 AdvancedProtectionManager API 整合這項功能,偵測模式狀態,讓應用程式在使用者啟用時自動採用強化安全措施,或限制高風險功能。
PQC APK 簽署
Android 現在支援混合式 APK 簽署配置,可保護應用程式的簽署身分,抵禦未來可能出現的量子運算攻擊威脅。這項功能推出新的 APK 簽署配置,可將傳統簽署金鑰 (例如 RSA 或 EC) 與新的後量子密碼編譯 (PQC) 演算法 (ML-DSA) 配對。
這種混合式做法可確保應用程式能抵禦未來的量子攻擊,同時與舊版 Android 和依賴傳統簽章驗證的裝置完全回溯相容。
對開發人員的影響
- 使用 Play 應用程式簽署功能的應用程式:如果您使用 Play 應用程式簽署功能,可以等待 Google Play 提供選項,使用 Google Play 產生的 PQC 金鑰升級混合簽章,確保應用程式受到保護,不必手動管理金鑰。
- 使用自行管理金鑰的應用程式:自行管理簽署金鑰的開發人員,可以使用更新後的 Android 建構工具 (例如 apksigner),輪替至混合式身分,將 PQC 金鑰與新的傳統金鑰結合。(您必須建立新的傳統金鑰,無法重複使用舊金鑰)。
連線能力
Android 17 新增下列功能,可改善裝置和應用程式連線能力。
受限的衛星網路
實作最佳化功能,讓應用程式在低頻寬的衛星網路上也能有效運作。
使用者體驗和系統 UI
Android 17 包含下列異動,可提升使用者體驗。
專屬 Google 助理音量串流
Android 17 為小幫手應用程式推出專屬的 Google 助理音量串流,可使用 USAGE_ASSISTANT 播放。這項異動會將 Google 助理音訊與標準媒體串流分離,讓使用者能分別控制這兩種音量。這樣一來,您就能在將媒體播放靜音的同時,仍能聽見 Google 助理的回應,反之亦然。
如果小幫手應用程式可存取新的 MODE_ASSISTANT_CONVERSATION 音訊模式,就能進一步提升音量控制的一致性。Google 助理應用程式可以使用這個模式,向系統提供有關有效 Google 助理工作階段的提示,確保 Google 助理串流可在有效 USAGE_ASSISTANT 播放以外或透過連線的藍牙周邊裝置進行控制。
Handoff
接力是 Android 17 即將推出的新功能和 API,應用程式開發人員可以整合這項功能,為使用者提供跨裝置接續互通功能。使用者可以在一部 Android 裝置上啟動應用程式活動,然後轉移到另一部 Android 裝置。接力功能會在使用者裝置的背景執行,並透過接收裝置上的各種進入點 (例如啟動器和工作列),顯示使用者其他鄰近裝置上的可用活動。
如果接收裝置已安裝並可使用相同的原生 Android 應用程式,應用程式可以指定 Handoff 啟動該應用程式。在這個應用程式對應用程式流程中,使用者會透過深層連結前往指定活動。或者,您也可以將應用程式到網頁的接力功能做為備用選項,或直接透過網址接力功能導入。
接力支援是以個別活動為基礎實作。如要啟用接力功能,請呼叫活動的 setHandoffEnabled() 方法。您可能需要將額外資料與接力作業一併傳遞,以便接收裝置上重新建立的活動還原適當狀態。實作 onHandoffActivityDataRequested() 回呼,傳回 HandoffActivityData 物件,其中包含詳細資料,指定接力功能應如何處理及在接收裝置上重新建立活動。
最新即時資訊 - 語意顏色 API
Android 17 的即時更新功能推出語意著色 API,支援具有通用意義的顏色。
下列類別支援語意著色:
NotificationNotification.MetricNotification.ProgressStyle.PointNotification.ProgressStyle.Segment
著色
- 綠色:與安全相關。這個顏色應該用於讓使用者知道您處於安全情況。
- 橘色:用於表示注意和標示實體危害。如果使用者需要留意設定,以提高保護效果,就應該使用這個顏色。
- 紅色:通常表示危險、停止。應在需要使用者緊急注意時顯示。
- 藍色:中性色,用於資訊內容,且應與其他內容區隔。
以下範例說明如何將語意樣式套用至通知中的文字:
val ssb = SpannableStringBuilder()
.append("Colors: ")
.append("NONE", Notification.createSemanticStyleAnnotation(SEMANTIC_STYLE_UNSPECIFIED), 0)
.append(", ")
.append("INFO", Notification.createSemanticStyleAnnotation(SEMANTIC_STYLE_INFO), 0)
.append(", ")
.append("SAFE", Notification.createSemanticStyleAnnotation(SEMANTIC_STYLE_SAFE), 0)
.append(", ")
.append("CAUTION", Notification.createSemanticStyleAnnotation(SEMANTIC_STYLE_CAUTION), 0)
.append(", ")
.append("DANGER", Notification.createSemanticStyleAnnotation(SEMANTIC_STYLE_DANGER), 0)
Notification.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_icon)
.setContentTitle("Hello World!")
.setContentText(ssb)
.setOngoing(true)
.setRequestPromotedOngoing(true)
Android 17 的 UWB Downlink-TDoA API
下行到達時間差 (DL-TDoA) 測距功能可讓裝置測量信號的相對到達時間,判斷其相對於多個錨點的位置。下列程式碼片段示範如何初始化測距管理工具、驗證裝置功能,以及啟動 DL-TDoA 工作階段:
Kotlin
class RangingApp {
fun initDlTdoa(context: Context) {
// Initialize the Ranging Manager
val rangingManager = context.getSystemService(RangingManager::class.java)
// Register for device capabilities
val capabilitiesCallback = object : RangingManager.RangingCapabilitiesCallback {
override fun onRangingCapabilities(capabilities: RangingCapabilities) {
// Make sure Dl-TDoA is supported before starting the session
if (capabilities.uwbCapabilities != null && capabilities.uwbCapabilities!!.isDlTdoaSupported) {
startDlTDoASession(context)
}
}
}
rangingManager.registerCapabilitiesCallback(Executors.newSingleThreadExecutor(), capabilitiesCallback)
}
fun startDlTDoASession(context: Context) {
// Initialize the Ranging Manager
val rangingManager = context.getSystemService(RangingManager::class.java)
// Create session and configure parameters
val executor = Executors.newSingleThreadExecutor()
val rangingSession = rangingManager.createRangingSession(executor, RangingSessionCallback())
val rangingRoundIndexes = byteArrayOf(0)
val config: ByteArray = byteArrayOf() // OOB config data
val params = DlTdoaRangingParams.createFromFiraConfigPacket(config, rangingRoundIndexes)
val rangingDevice = RangingDevice.Builder().build()
val rawTagDevice = RawRangingDevice.Builder()
.setRangingDevice(rangingDevice)
.setDlTdoaRangingParams(params)
.build()
val dtTagConfig = RawDtTagRangingConfig.Builder(rawTagDevice).build()
val preference = RangingPreference.Builder(DEVICE_ROLE_DT_TAG, dtTagConfig)
.setSessionConfig(SessionConfig.Builder().build())
.build()
// Start the ranging session
rangingSession.start(preference)
}
}
private class RangingSessionCallback : RangingSession.Callback {
override fun onDlTdoaResults(peer: RangingDevice, measurement: DlTdoaMeasurement) {
// Process measurement results here
}
}
Java
public class RangingApp {
public void initDlTdoa(Context context) {
// Initialize the Ranging Manager
RangingManager rangingManager = context.getSystemService(RangingManager.class);
// Register for device capabilities
RangingManager.CapabilitiesCallback capabilitiesCallback = new RangingManager.RangingCapabilitiesCallback() {
@Override
public void onRangingCapabilities(RangingCapabilities capabilities) {
// Make sure Dl-TDoA is supported before starting the session
if (capabilities.getUwbCapabilities() != null && capabilities.getUwbCapabilities().isDlTdoaSupported()) {
startDlTDoASession(context);
}
}
};
rangingManager.registerCapabilitiesCallback(Executors.newSingleThreadExecutor(), capabilitiesCallback);
}
public void startDlTDoASession(Context context) {
RangingManager rangingManager = context.getSystemService(RangingManager.class);
// Create session and configure parameters
Executor executor = Executors.newSingleThreadExecutor();
RangingSession rangingSession = rangingManager.createRangingSession(executor, new RangingSessionCallback());
byte[] rangingRoundIndexes = new byte[] {0};
byte[] config = new byte[0]; // OOB config data
DlTdoaRangingParams params = DlTdoaRangingParams.createFromFiraConfigPacket(config, rangingRoundIndexes);
RangingDevice rangingDevice = new RangingDevice.Builder().build();
RawRangingDevice rawTagDevice = new RawRangingDevice.Builder()
.setRangingDevice(rangingDevice)
.setDlTdoaRangingParams(params)
.build();
RawDtTagRangingConfig dtTagConfig = new RawDtTagRangingConfig.Builder(rawTagDevice).build();
RangingPreference preference = new RangingPreference.Builder(DEVICE_ROLE_DT_TAG, dtTagConfig)
.setSessionConfig(new SessionConfig.Builder().build())
.build();
// Start the ranging session
rangingSession.start(preference);
}
private static class RangingSessionCallback implements RangingSession.Callback {
@Override
public void onDlTdoaResults(RangingDevice peer, DlTdoaMeasurement measurement) {
// Process measurement results here
}
}
}
頻外 (OOB) 設定
以下程式碼片段提供 Wi-Fi 和 BLE 的 DL-TDoA OOB 設定資料範例:
Java
// Wifi Configuration
byte[] wifiConfig = {
(byte) 0xDD, (byte) 0x2D, (byte) 0x5A, (byte) 0x18, (byte) 0xFF, // Header
(byte) 0x5F, (byte) 0x19, // FiRa Sub-Element
(byte) 0x02, (byte) 0x00, // Profile ID
(byte) 0x06, (byte) 0x02, (byte) 0x20, (byte) 0x08, // MAC Address
(byte) 0x14, (byte) 0x01, (byte) 0x0C, // Preamble Index
(byte) 0x27, (byte) 0x02, (byte) 0x08, (byte) 0x07, // Vendor ID
(byte) 0x28, (byte) 0x06, (byte) 0xCA, (byte) 0xC8, (byte) 0xA6, (byte) 0xF7, (byte) 0x6F, (byte) 0x08, // Static STS IV
(byte) 0x08, (byte) 0x02, (byte) 0x60, (byte) 0x09, // Slot Duration
(byte) 0x1B, (byte) 0x01, (byte) 0x0A, // Slots per RR
(byte) 0x09, (byte) 0x04, (byte) 0xE8, (byte) 0x03, (byte) 0x00, (byte) 0x00, // Duration
(byte) 0x9F, (byte) 0x04, (byte) 0x67, (byte) 0x45, (byte) 0x23, (byte) 0x01 // Session ID
};
// BLE Configuration
byte[] bleConfig = {
(byte) 0x2D, (byte) 0x16, (byte) 0xF4, (byte) 0xFF, // Header
(byte) 0x5F, (byte) 0x19, // FiRa Sub-Element
(byte) 0x02, (byte) 0x00, // Profile ID
(byte) 0x06, (byte) 0x02, (byte) 0x20, (byte) 0x08, // MAC Address
(byte) 0x14, (byte) 0x01, (byte) 0x0C, // Preamble Index
(byte) 0x27, (byte) 0x02, (byte) 0x08, (byte) 0x07, // Vendor ID
(byte) 0x28, (byte) 0x06, (byte) 0xCA, (byte) 0xC8, (byte) 0xA6, (byte) 0xF7, (byte) 0x6F, (byte) 0x08, // Static STS IV
(byte) 0x08, (byte) 0x02, (byte) 0x60, (byte) 0x09, // Slot Duration
(byte) 0x1B, (byte) 0x01, (byte) 0x0A, // Slots per RR
(byte) 0x09, (byte) 0x04, (byte) 0xE8, (byte) 0x03, (byte) 0x00, (byte) 0x00, // Duration
(byte) 0x9F, (byte) 0x04, (byte) 0x67, (byte) 0x45, (byte) 0x23, (byte) 0x01 // Session ID
};
如果 OOB 設定遺失而無法使用,或需要變更 OOB 設定中沒有的預設值,您可以使用 DlTdoaRangingParams.Builder 建構參數,如以下程式碼片段所示。您可以使用這些參數取代 DlTdoaRangingParams.createFromFiraConfigPacket():
Kotlin
val dlTdoaParams = DlTdoaRangingParams.Builder(1)
.setComplexChannel(UwbComplexChannel.Builder()
.setChannel(9).setPreambleIndex(10).build())
.setDeviceAddress(deviceAddress)
.setSessionKeyInfo(byteArrayOf(0x01, 0x02, 0x03, 0x04))
.setRangingIntervalMillis(240)
.setSlotDuration(UwbRangingParams.DURATION_2_MS)
.setSlotsPerRangingRound(20)
.setRangingRoundIndexes(byteArrayOf(0x01, 0x05))
.build()
Java
DlTdoaRangingParams dlTdoaParams = new DlTdoaRangingParams.Builder(1)
.setComplexChannel(new UwbComplexChannel.Builder()
.setChannel(9).setPreambleIndex(10).build())
.setDeviceAddress(deviceAddress)
.setSessionKeyInfo(new byte[]{0x01, 0x02, 0x03, 0x04})
.setRangingIntervalMillis(240)
.setSlotDuration(UwbRangingParams.DURATION_2_MS)
.setSlotsPerRangingRound(20)
.setRangingRoundIndexes(new byte[]{0x01, 0x05})
.build();