기능 및 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 introduces new JobDebugInfo APIs to help developers debug their JobScheduler jobs--why they aren't running, how long they ran for, and other aggregated information.

The first method of the expanded JobDebugInfo APIs is getPendingJobReasonStats(), which returns a map of reasons why the job was in a pending execution state and their respective cumulative pending durations. This method joins the getPendingJobReasonsHistory() and getPendingJobReasons() methods to give you insight into why a scheduled job is not running as expected, but simplifies information retrieval by making both duration and job reason available in a single method.

For example, for a specified jobId, the method might return PENDING_JOB_REASON_CONSTRAINT_CHARGING and a duration of 60000 ms, indicating the job was pending for 60000ms due to the charging constraint not being satisfied.

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

Android 17 에서는 AlarmManager.setExactAndAllowWhileIdle 대신 PendingIntent을 허용하는 새로운 OnAlarmListener 변형이 도입되었습니다. 이 새로운 콜백 기반 메커니즘은 현재 지속적인 웨이크 잠금에 의존하여 소켓 연결을 유지하는 메시지 앱과 같은 주기적인 작업을 실행하는 앱에 적합합니다.

개인 정보 보호

Android 17에는 사용자 개인 정보 보호를 개선하기 위한 다음과 같은 새로운 기능이 포함되어 있습니다.

Encrypted Client Hello (ECH) 플랫폼 지원

Android 17에서는 네트워크 통신을 위한 중요한 개인 정보 보호 개선사항인 Encrypted Client Hello (ECH)에 대한 플랫폼 지원을 도입합니다. ECH는 초기 TLS 핸드셰이크 중에 서버 이름 표시 (SNI)를 암호화하는 TLS 1.3 확장 프로그램입니다. 이 암호화는 네트워크 중개자가 앱이 연결되는 특정 도메인을 식별하기 더 어렵게 만들어 사용자 개인 정보를 보호하는 데 도움이 됩니다.

이제 플랫폼에 네트워킹 라이브러리가 ECH를 구현하는 데 필요한 API가 포함됩니다. 여기에는 ECH 구성을 포함하는 HTTPS DNS 레코드를 쿼리하는 DnsResolver의 새로운 기능과 도메인에 연결할 때 이러한 구성을 전달하여 ECH를 사용 설정하는 Conscrypt's SSLEngines 및 SSLSockets의 새로운 메서드가 포함됩니다. 개발자는 네트워크 보안 구성 파일 내의 새로운 <domainEncryption> 요소를 통해 ECH 환경설정을 구성할 수 있습니다. 이 요소는 전역적으로 또는 도메인별로 적용됩니다.

HttpEngine, WebView, OkHttp와 같은 인기 있는 네트워킹 라이브러리는 향후 업데이트에서 이러한 플랫폼 API를 통합하여 앱이 ECH를 채택하고 사용자 개인 정보를 보호하는 데 도움이 될 것으로 예상됩니다.

자세한 내용은 Encrypted Client Hello 문서를 참고하세요.

Android 연락처 선택 도구

Android 연락처 선택 도구는 사용자가 앱과 연락처를 공유할 수 있는 표준화된 탐색 가능한 인터페이스입니다. Android 17 (API 수준 37) 이상을 실행하는 기기에서 사용할 수 있는 선택 도구는 광범위한 READ_CONTACTS 권한에 대한 개인 정보 보호 대안을 제공합니다. 사용자의 전체 주소록에 대한 액세스를 요청하는 대신 앱은 전화번호나 이메일 주소와 같이 필요한 데이터 필드를 지정하고 사용자는 공유할 특정 연락처를 선택합니다. 이렇게 하면 앱에 선택한 데이터에 대한 읽기 액세스 권한만 부여되므로 UI를 빌드하거나 유지관리하지 않고도 내장 검색, 프로필 전환, 다중 선택 기능을 통해 일관된 사용자 환경을 제공하면서 세부적인 제어가 가능합니다.

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

보안

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

Android 고급 보호 모드 (AAPM)

Android Advanced Protection Mode offers Android users a powerful new set of security features, marking a significant step in safeguarding users—particularly those at higher risk—from sophisticated attacks. Designed as an opt-in feature, AAPM is activated with a single configuration setting that users can turn on at any time to apply an opinionated set of security protections.

These core configurations include blocking app installation from unknown sources (sideloading), restricting USB data signaling, and mandating Google Play Protect scanning, which significantly reduces the device's attack surface area. Developers can integrate with this feature using the AdvancedProtectionManager API to detect the mode's status, enabling applications to automatically adopt a hardened security posture or restrict high-risk functionality when a user has opted in.

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에서는 USAGE_ASSISTANT로 재생하기 위해 어시스턴트 앱 전용 어시스턴트 볼륨 스트림을 도입합니다. 이 변경사항은 어시스턴트 오디오를 표준 미디어 스트림에서 분리하여 사용자가 두 볼륨을 모두 격리된 방식으로 제어할 수 있도록 합니다. 이를 통해 어시스턴트 응답의 가청성을 유지하면서 미디어 재생을 음소거하는 등의 시나리오가 가능합니다.

새로운 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 다운링크-TDoA API

Downlink Time Difference of Arrival (DL-TDoA) ranging lets a device determine its position relative to multiple anchors by measuring the relative arrival times of signals.

The following snippet demonstrates how to initialize the [Ranging Manager][ranging-manager-ref], verify device capabilities, and start a DL-TDoA session:

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

Out-of-Band (OOB) Configurations

The following snippet provides an example of DL-TDoA OOB configuration data for Wi-Fi and BLE:

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

If you can't use an OOB configuration because it is missing, or if you need to change default values that aren't in the OOB config, you can build parameters with DlTdoaRangingParams.Builder as shown in the following snippet. You can use these parameters in place of 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();

[ranging-manager-ref]: /reference/android/ranging/RangingManager