功能與 API

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 回呼會在系統強制執行任何措施前發生。舉例來說,在應用程式因超出記憶體限制而遭系統終止前,這項功能可協助開發人員收集偵錯資料。

fun registerAnomalyTrigger(context: Context) {
    val profilingManager =
        context.getSystemService(ProfilingManager::class.java) ?: return
    val triggers = listOf(
        ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANOMALY).build()
    )
    val callbackExecutor: Executor = Executors.newSingleThreadExecutor()
    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
            // Upload profile result to server for further analysis
            profilingResult.resultFilePath?.let { setupProfileUploadWorker(it) }
        }
    }
    profilingManager.registerForAllProfilingResults(
        callbackExecutor,
        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 introduces platform support for Encrypted Client Hello (ECH), a significant privacy enhancement for network communications. ECH is a TLS 1.3 extension that encrypts the Server Name Indication (SNI) during the initial TLS handshake. This encryption helps protect user privacy by making it more difficult for network intermediaries to identify the specific domain an app is connecting to.

The platform now includes the necessary APIs for networking libraries to implement ECH. This includes new capabilities in DnsResolver to query for HTTPS DNS records containing ECH configurations, and new methods in Conscrypt's SSLEngines and SSLSockets to enable ECH by passing in these configurations when connecting to a domain. Developers can configure ECH preferences, such as enabling it opportunistically or mandating its use, through the new <domainEncryption> element within the Network Security Configuration file, applicable globally or on a per-domain basis.

Popular networking libraries such as HttpEngine, WebView, and OkHttp are expected to integrate these platform APIs in future updates, making it easier for apps to adopt ECH and enhance user privacy.

For more information, see the Encrypted Client Hello documentation.

Android 聯絡人選擇工具

Android 联系人选择工具是一个标准化的可浏览界面,可供用户与您的应用分享联系人。该选择工具适用于搭载 Android 17(API 级别 37)或更高版本的设备,可替代广泛的 READ_CONTACTS 权限,从而保护隐私。您的应用无需请求访问用户的整个通讯簿,而是指定所需的数据字段(例如电话号码或邮箱),然后由用户选择要分享的特定联系人。这会向您的应用授予对所选数据的读取权限,从而确保精细控制,同时通过内置搜索、个人资料切换和多选功能提供一致的用户体验,而无需构建或维护界面。

如需了解详情,请参阅联系人选择工具文档。

安全性

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 新增下列功能,可改善裝置和應用程式連線能力。

受限的衛星網路

Implements optimizations to enable apps to function effectively over low-bandwidth satellite networks.

使用者體驗和系統 UI

Android 17 包含下列異動,可提升使用者體驗。

專屬 Google 助理音量串流

Android 17 为辅助应用引入了专用的 Google 助理音量流,以便使用 USAGE_ASSISTANT 进行播放。此项变更将 Google 助理音频与标准媒体流分离开来,让用户可以分别控制这两种音量。这样一来,您就可以在静音媒体播放的同时保持 Google 助理回答的可听性,反之亦然。

能够访问新 MODE_ASSISTANT_CONVERSATION 音频模式的辅助应用可以进一步提高音量控制的一致性。助理应用可以使用此模式向系统提供有关活跃助理会话的提示,确保可以在活跃的 USAGE_ASSISTANT 播放之外或通过连接的蓝牙外围设备控制助理音频流。

Handoff

Handoff is a new feature and API coming to Android 17 that app developers can integrate with to provide cross-device continuity for their users. It allows the user to start an app activity on one Android device and transition it to another Android device. Handoff runs in the background of a user's device and surfaces available activities from the user's other nearby devices through various entry points, like the launcher and taskbar, on the receiving device.

Apps can designate Handoff to launch the same native Android app, if it is installed and available on the receiving device. In this app-to-app flow, the user is deep-linked to the designated activity. Alternatively, app-to-web Handoff can be offered as a fallback option or directly implemented with URL Handoff.

Handoff support is implemented on a per-activity basis. To enable Handoff, call the setHandoffEnabled() method for the activity. Additional data may need to be passed along with the handoff so the recreated activity on the receiving device can restore appropriate state. Implement the onHandoffActivityDataRequested() callback to return a HandoffActivityData object which contains details that specify how Handoff should handle and recreate the activity on the receiving device.

最新即時資訊 - 語意顏色 API

Android 17 的即時更新功能推出語意著色 API,支援具有通用意義的顏色。

下列類別支援語意著色:

著色

  • 綠色:與安全相關。這個顏色應該用於讓使用者知道您處於安全情況。
  • 橘色:用於表示注意和標示實體危害。如果使用者需要留意設定,以提高保護效果,就應該使用這個顏色。
  • 紅色:通常表示危險、停止。應在需要使用者緊急注意時顯示。
  • 藍色:中性色,用於資訊內容,且應與其他內容區隔。

以下範例說明如何將語意樣式套用至通知中的文字:

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