기능 및 API

Android 17에서는 개발자를 위한 훌륭한 새 기능과 API가 도입됩니다. 다음 섹션에서는 이러한 기능을 요약하여 관련 API를 시작하는 데 도움을 드립니다.

새로운 API, 수정된 API, 삭제된 API에 관한 자세한 목록은 API diff 보고서를 참고하세요. 새로운 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 정의 메모리 제한을 위반하면 비정상 트리거를 통해 개발자가 앱별 힙 덤프를 수신하여 메모리 문제를 식별하고 수정할 수 있습니다. 또한 과도한 바인더 스팸의 경우 이상치 트리거는 바인더 트랜잭션에 관한 스택 샘플링 프로필을 제공합니다.

이 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 毫秒。

유휴 상태에서 허용 알람의 리스너 지원으로 웨이크 잠금 감소

Android 17 introduces a new variant of AlarmManager.setExactAndAllowWhileIdle that accepts an OnAlarmListener instead of a PendingIntent. This new callback-based mechanism is ideal for apps that currently rely on continuous wakelocks to perform periodic tasks, such as messaging apps maintaining socket connections.

개인 정보 보호

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 권한에 대한 개인 정보 보호 대안을 제공합니다. 사용자의 전체 주소록에 대한 액세스를 요청하는 대신 앱은 전화번호나 이메일 주소와 같이 필요한 데이터 필드를 지정하고 사용자는 공유할 특정 연락처를 선택합니다. 이렇게 하면 앱에 선택한 데이터에 대한 읽기 액세스 권한만 부여되므로 UI를 빌드하거나 유지관리하지 않고도 내장 검색, 프로필 전환, 다중 선택 기능을 통해 일관된 사용자 환경을 제공하면서 세부적인 제어가 가능합니다.

자세한 내용은 연락처 선택기 문서를 참고하세요.

보안

Android 17에서는 기기 및 앱 보안을 개선하기 위한 다음과 같은 새로운 기능이 추가되었습니다.

Android 고급 보호 모드 (AAPM)

Android 고급 보호 모드는 Android 사용자에게 강력한 새로운 보안 기능을 제공하여 정교한 공격으로부터 사용자를 보호하는 데 중요한 역할을 합니다. 특히 위험도가 높은 사용자를 보호하는 데 효과적입니다. 선택 기능으로 설계된 AAPM은 사용자가 언제든지 사용 설정하여 의견이 반영된 보안 보호 세트를 적용할 수 있는 단일 구성 설정으로 활성화됩니다.

이러한 핵심 구성에는 알 수 없는 소스에서 앱 설치 차단(사이드로드)과 USB 데이터 신호 제한, Google Play 프로텍트 검사 의무화가 포함되어 기기의 공격 표면적을 크게 줄입니다. 개발자는 AdvancedProtectionManager API를 사용하여 이 기능과 통합하여 모드의 상태를 감지할 수 있으므로 사용자가 선택한 경우 애플리케이션이 강화된 보안 자세를 자동으로 채택하거나 위험도가 높은 기능을 제한할 수 있습니다.

PQC APK 서명

이제 Android는 양자 컴퓨팅을 활용하는 공격의 잠재적 위협으로부터 앱의 서명 ID를 보호하기 위해 하이브리드 APK 서명 스키마를 지원합니다. 이 기능은 기존 서명 키 (예: RSA 또는 EC)를 새로운 양자 내성 암호 (PQC) 알고리즘 (ML-DSA)과 페어링할 수 있는 새로운 APK 서명 스키마를 도입합니다.

이 하이브리드 접근 방식을 사용하면 기존 서명 확인에 의존하는 이전 Android 버전 및 기기와의 완전한 이전 버전 호환성을 유지하면서 향후 양자 공격으로부터 앱을 안전하게 보호할 수 있습니다.

개발자에게 미치는 영향

  • Play 앱 서명을 사용하는 앱: Play 앱 서명을 사용하는 경우 Google Play에서 생성한 PQC 키를 사용하여 하이브리드 서명을 업그레이드하는 옵션을 Google Play에서 제공할 때까지 기다릴 수 있습니다. 이렇게 하면 수동 키 관리 없이 앱을 보호할 수 있습니다.
  • 자체 관리 키를 사용하는 앱: 자체 서명 키를 관리하는 개발자는 업데이트된 Android 빌드 도구 (예: apksigner)를 활용하여 PQC 키와 새 기존 키를 결합하는 하이브리드 ID로 순환할 수 있습니다. (새 기존 키를 만들어야 하며 이전 키는 재사용할 수 없습니다.)

연결

Android 17에서는 기기 및 앱 연결을 개선하기 위한 다음과 같은 기능이 추가되었습니다.

제약된 위성 네트워크

实现优化,使应用能够在低带宽卫星网络上有效运行。

사용자 환경 및 시스템 UI

Android 17에는 사용자 환경을 개선하기 위한 다음과 같은 변경사항이 포함되어 있습니다.

전용 어시스턴트 볼륨 스트림

Android 17 introduces a dedicated Assistant volume stream for Assistant apps, for playback with USAGE_ASSISTANT. This change decouples Assistant audio from the standard media stream, providing users with isolated control over both volumes. This enables scenarios such as muting media playback while maintaining audibility for Assistant responses, and the other way around.

Assistant apps with access to the new MODE_ASSISTANT_CONVERSATION audio mode can further improve the volume control consistency. Assistant apps can use this mode to provide a hint to the system about an active Assistant session, ensuring the Assistant stream can be controlled outside of the active USAGE_ASSISTANT playback or with connected Bluetooth peripherals.

Handoff

切换是 Android 17 中新增的一项功能和 API,应用开发者可以将其集成到应用中,以便为用户提供跨设备连续性。它允许用户在一个 Android 设备上启动应用 activity,然后将其转移到另一个 Android 设备。Handoff 在用户设备的后台运行,并通过各种入口点(例如接收设备上的启动器和任务栏)显示用户附近其他设备上的可用活动。

应用可以指定 Handoff 来启动相同的原生 Android 应用(如果该应用已安装在接收设备上且可供使用)。在此应用到应用流程中,用户通过深层链接跳转到指定 activity。或者,应用到网站切换功能可以作为后备选项提供,也可以通过网址切换功能直接实现。

切换支持是按 activity 实现的。如需启用 Handoff,请针对 activity 调用 setHandoffEnabled() 方法。可能需要随切换传递其他数据,以便接收设备上重新创建的 activity 可以恢复适当的状态。实现 onHandoffActivityDataRequested() 回调以返回 HandoffActivityData 对象,该对象包含用于指定 Handoff 应如何处理并在接收设备上重新创建 activity 的详细信息。

실시간 업데이트 - 시맨틱 색상 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 다운링크-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()

자바

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