디스플레이 지원

이러한 디스플레이 관련 영역에 적용된 업데이트는 아래와 같습니다.

활동 및 디스플레이 크기 조정

앱이 멀티 윈도우 모드나 크기 조정을 지원하지 않을 수도 있음을 나타내기 위해 활동은 resizeableActivity=false 속성을 사용합니다. 활동 크기를 조정할 때 앱에서 자주 발생하는 문제는 다음과 같습니다.

  • 활동은 앱이나 다른 비시각적 구성요소와 구성이 다를 수 있습니다. 가장 흔한 실수는 앱 컨텍스트에서 디스플레이 측정항목을 읽는 것입니다. 반환된 값은 활동이 표시되는 공개 영역 측정항목에 맞게 조정되지 않습니다.
  • 활동은 크기 조정 및 충돌을 처리하거나 왜곡된 UI를 표시하거나 인스턴스 상태를 저장하지 않은 상태에서 재실행으로 인해 상태를 잃을 수 없습니다.
  • 앱은 창 위치에 비례하는 좌표 대신 절대 입력 좌표를 사용하려고 시도할 수 있으며, 이 경우 멀티 윈도우의 입력이 깨질 수 있습니다.

Android 7 이상에서는 앱이 항상 전체 화면 모드에서 실행되도록 resizeableActivity=false로 설정할 수 있습니다. 이 경우 플랫폼에서는 크기 조정이 불가한 활동이 화면 분할 모드로 이동하지 못하도록 합니다. 사용자가 화면 분할 모드의 런처에서 크기 조정이 불가한 활동을 호출하려고 하면 플랫폼에서 화면 분할 모드를 종료하고 크기 조정이 불가한 활동을 전체 화면 모드로 실행합니다.

매니페스트에서 이 속성을 false로 명시적으로 설정한 앱은 호환성 모드가 적용되지 않은 이상 멀티 윈도우 모드로 실행하면 안 됩니다.

  • 모든 활동과 비활동 구성요소가 포함된 프로세스에 같은 구성이 적용됩니다.
  • 앱 호환 디스플레이와 관련된 CDD 요구사항을 충족하는 구성이 적용됩니다.

Android 10에서는 크기 조정이 불가한 활동이 화면 분할 모드로 전환되지 않도록 플랫폼에서 계속 막지만, 활동에서 고정된 방향이나 가로세로 비율을 선언한 경우에는 이러한 활동이 일시적으로 확장될 수 있습니다. 아니면 Android 9 이하에서처럼 활동 크기가 조정되어 전체 화면을 채웁니다.

기본 구현은 다음 정책을 적용합니다.

활동이 android:resizeableActivity 속성을 사용하여 멀티 윈도우와 호환된다고 선언되고 이 활동이 아래에 설명된 조건 중 하나에 부합하는 경우에는 적용된 화면 구성을 변경해야 할 때 활동과 프로세스가 원래 구성으로 저장되고 사용자에게는 앱 프로세스를 재실행하여 업데이트된 화면 구성을 사용할 수 있도록 어포던스가 제공됩니다.

  • android:screenOrientation을 적용하여 고정된 방향입니다.
  • 앱이 API 수준을 타겟팅하여 최대 또는 최소 가로세로 비율 기본값을 가지거나 가로세로 비율을 명시적으로 선언합니다.

이 그림은 가로세로 비율이 선언된 크기 조정이 불가한 활동을 보여줍니다. 기기를 접을 때 창은 영역에 맞게 축소되는 반면 가로세로 비율은 적절한 레터박스를 사용하여 유지됩니다. 또한 활동의 표시 영역이 변경될 때마다 사용자에게 활동 재시작 옵션이 제공됩니다.

기기를 펼치면 활동의 구성, 크기와 가로세로 비율이 변경되지 않지만 활동 재시작 옵션은 표시됩니다.

resizeableActivity가 설정되지 않았거나 true로 설정된 경우에는 앱이 크기 조정을 온전히 지원합니다.

구현

코드에서는 고정된 방향이나 가로세로 비율이 적용된 크기 조정이 불가한 활동을 크기 호환 모드(SCM)라고 부릅니다. 조건은 ActivityRecord#shouldUseSizeCompatMode()에서 정의됩니다. SCM 활동이 실행되면 화면 관련 구성(크기 또는 밀도 등)이 요청된 재정의 구성에 고정됩니다. 따라서 활동이 더 이상 현재의 디스플레이 구성에 종속되지 않습니다.

전체 화면을 채울 수 없는 SCM 활동은 상단에 정렬되며 가로 가운데 맞춤으로 설정됩니다. 활동 경계는 AppWindowToken#calculateCompatBoundsTransformation()으로 계산됩니다.

SCM 활동이 컨테이너와 다른 화면 구성을 사용하면(예: 디스플레이 크기가 조정되었거나 활동이 다른 디스플레이로 이동한 경우) ActivityRecord#inSizeCompatMode()는 true이고 시스템 UI의 SizeCompatModeActivityController는 프로세스 재시작 버튼 표시를 위한 콜백을 수신합니다.

디스플레이 크기 및 가로세로 비율

Android 10에서는 길고 얇은 화면의 높은 비율부터 1:1 비율에 이르는 새로운 가로세로 비율을 지원합니다. 앱은 처리 가능한 화면의 ApplicationInfo#maxAspectRatioApplicationInfo#minAspectRatio를 정의할 수 있습니다.

Android 10의 앱 비율

그림 1. Android 10에서 지원되는 앱 비율의 예

기기 구현에는 Android 9 이하의 요구사항(최소 2.5인치의 너비 또는 높이, 최소 320DP의 smallestScreenWidth)보다 작은 크기와 해상도를 가진 보조 디스플레이가 포함될 수 있지만 이렇게 작은 디스플레이를 지원하기로 선택한 활동만 여기에 배치할 수 있습니다.

앱은 타겟 디스플레이 크기와 같거나 작은 최소 지원 크기를 선언하여 이를 선택할 수 있습니다. 이렇게 하려면 AndroidManifest에서 android:minHeightandroid:minWidth 활동 레이아웃 속성을 사용하세요.

디스플레이 정책

Android 10에서는 PhoneWindowManager의 기본 WindowManagerPolicy 구현에서 특정 디스플레이 정책을 분리한 후 다음과 같은 디스플레이별 클래스로 이동합니다.

  • 디스플레이 상태 및 회전
  • 일부 키 및 모션 이벤트 추적
  • 시스템 UI 및 장식 창

Android 9 이하에서는 PhoneWindowManager 클래스가 디스플레이 정책, 상태 및 설정, 회전, 장식 창 프레임 추적 등을 처리했습니다. Android 10에서는 DisplayRotation으로 옮겨진 회전 추적을 제외한 대부분의 요소가 DisplayPolicy 클래스로 옮겨졌습니다.

디스플레이 창 설정

Android 10에서는 구성 가능한 디스플레이별 창 지정 설정이 다음을 포함하도록 확장되었습니다.

  • 기본 디스플레이 창 모드
  • 오버스캔 값
  • 사용자 회전 및 회전 모드
  • 크기, 밀도 및 확장 모드 강제 사용
  • 콘텐츠 삭제 모드(디스플레이가 삭제된 경우)
  • 시스템 장식 및 IME 지원

DisplayWindowSettings 클래스에는 이러한 옵션과 관련된 설정이 포함되며, 설정이 변경될 때마다 display_settings.xml/data 파티션에 위치한 디스크에서 지속됩니다. 자세한 내용은 DisplayWindowSettings.AtomicFileStorageDisplayWindowSettings#writeSettings()를 참고하세요. 기기 제조업체는 기기 구성의 display_settings.xml에 기본값을 제공할 수 있습니다. 하지만 파일이 /data에 저장되기 때문에 파일이 삭제되는 경우 복구하기 위한 추가 로직이 필요할 수도 있습니다.

기본적으로 Android 10에서는 설정을 지속할 때 DisplayInfo#uniqueId를 디스플레이의 식별자로 사용합니다. 모든 디스플레이의 uniqueId가 채워져야 합니다. 또한 실제 및 네트워크 디스플레이의 경우 안정적입니다. 실제 디스플레이의 포트를 식별자로 사용할 수도 있습니다. 이 식별자는 DisplayWindowSettings#mIdentifier에서 설정할 수 있습니다. 작성할 때마다 모든 설정이 작성됩니다. 따라서 저장소의 디스플레이 항목에 사용되는 키를 업데이트하는 편이 안전합니다. 자세한 내용은 정적 디스플레이 식별자를 참고하세요.

설정은 기록 측면의 사유로 인해 /data 디렉터리에서 지속됩니다. 원래 설정은 디스플레이 회전과 같은 사용자 설정에서 지속되었습니다.

정적 디스플레이 식별자

Android 9 이하에서는 프레임워크의 디스플레이를 위한 안정적인 식별자를 제공하지 않았으며, 디스플레이가 시스템에 추가되면 정적 카운터를 증분하여 Display#mDisplayId 또는 DisplayInfo#displayId를 생성했습니다. 시스템에서 같은 디스플레이를 추가하거나 삭제한 경우에는 다른 ID가 생성되었습니다.

기기에 부팅에서 사용 가능한 여러 개의 디스플레이가 있는 경우에는 시기에 따라 디스플레이에 다른 식별자를 할당할 수 있었습니다. Android 9 이하에는 DisplayInfo#uniqueId가 포함되었지만 디스플레이 구분을 위한 정보는 부족했습니다. 이는 내장 및 외장 디스플레이를 나타내기 위해 실제 디스플레이가 local:0 또는 local:1로 식별되었기 때문입니다.

Android 10에서는 안정적인 식별자를 추가하고 로컬, 네트워크 및 가상 디스플레이를 구분하기 위해 DisplayInfo#uniqueId를 변경합니다.

디스플레이 유형 방식
로컬

local:<stable-id>
네트워크

network:<mac-address>
가상

virtual:<package-name-and-name>

uniqueId의 업데이트 외에도 DisplayInfo.address에는 모든 재부팅에 걸쳐 안정적인 디스플레이 식별자인 DisplayAddress가 포함됩니다. Android 10에서는 DisplayAddress가 실제 및 네트워크 디스플레이를 지원합니다. DisplayAddress.Physical에는 안정적인 디스플레이 ID(uniqueId와 동일)가 포함되며, DisplayAddress#fromPhysicalDisplayId()로 생성 가능합니다.

Android 10은 포트 정보(Physical#getPort())를 가져오기 위한 편리한 메서드도 제공합니다. 이 메서드는 프레임워크에서 디스플레이를 정적으로 식별하는 데 사용할 수 있습니다. 예를 들면 DisplayWindowSettings에 사용할 수 있습니다. DisplayAddress.Network에는 MAC 주소가 포함되며, DisplayAddress#fromMacAddress()로 생성 가능합니다.

이러한 추가 기능을 통해 기기 제조업체는 정적 다중 디스플레이 설정의 디스플레이를 식별하고 실제 디스플레이용 포트와 같은 정적 디스플레이 식별자를 사용하여 다양한 시스템 설정과 기능을 구성할 수 있습니다. 이러한 메서드는 숨겨지며 system_server 내에서만 사용하도록 의도됩니다.

불투명할 수 있으며 항상 안정적이지는 않은 HWC 디스플레이 ID를 부여하면 이 메서드는 디스플레이 출력의 실제 커넥터를 식별하는 8비트 포트 번호(플랫폼 관련)와 디스플레이의 EDID blob를 반환합니다. SurfaceFlinger는 EDID에서 제조업체 또는 모델 정보를 추출하여 프레임워크에 노출되는 안정적인 64비트 디스플레이 ID를 생성합니다. 이 메서드가 지원되지 않거나 무시되면 SurfaceFlinger는 레거시 MD 모드로 돌아갑니다. 위에서 설명한 것처럼 여기서 DisplayInfo#address는 null이며 DisplayInfo#uniqueId는 하드 코딩됩니다.

이 기능이 지원되는지 확인하려면 다음을 실행합니다.

$ dumpsys SurfaceFlinger --display-id
# Example output.
Display 21691504607621632 (HWC display 0): port=0 pnpId=SHP displayName="LQ123P1JX32"
Display 9834494747159041 (HWC display 2): port=1 pnpId=HWP displayName="HP Z24i"
Display 1886279400700944 (HWC display 1): port=2 pnpId=AUS displayName="ASUS MB16AP"

3개 이상의 디스플레이 사용

Android 9 이하에서 SurfaceFlinger 및 DisplayManagerService는 하드코딩 ID인 0 및 1을 포함하는 최대 2개의 실제 디스플레이가 존재한다고 가정했습니다.

Android 10부터 SurfaceFlinger에서 Hardware Composer(HWC) API를 사용하여 안정적인 ID를 생성하여 실제 디스플레이의 임의 개수를 관리할 수 있었습니다. 자세한 내용은 정적 디스플레이 식별자를 참고하세요.

프레임워크는 SurfaceControl#getPhysicalDisplayIds 또는 DisplayEventReceiver 핫플러그 이벤트에서 64비트 디스플레이 ID를 획득한 후 SurfaceControl#getPhysicalDisplayToken을 통해 실제 디스플레이의 IBinder 토큰을 조회할 수 있습니다.

Android 10 이하에서는 기본 내부 디스플레이가 TYPE_INTERNAL이며, 모든 보조 디스플레이는 연결 유형과 상관없이 TYPE_EXTERNAL로 신고됩니다. 따라서 추가적인 내부 디스플레이가 외부 디스플레이로 취급됩니다. 해결 방법으로 기기별 코드는 HWC가 알려졌고 포트 할당 논리가 예측 가능한 경우 DisplayAddress.Physical#getPort에 관해 가정합니다.

이 제한사항은 Android 11 이상에서 삭제되었습니다.

  • Android 11에서는 부팅 중에 보고되는 첫 번째 디스플레이가 기본 디스플레이입니다. 연결 유형(내부 및 외부)은 관련이 없습니다. 하지만 기본 디스플레이는 여전히 연결 해제할 수 없으며 실제로 내부 디스플레이여야 합니다. 일부 폴더블 스마트폰에는 내부 디스플레이가 여러 개 있습니다.
  • 보조 디스플레이는 연결 유형에 따라 Display.TYPE_INTERNAL 또는 Display.TYPE_EXTERNAL(각각 이전의 Display.TYPE_BUILT_INDisplay.TYPE_HDMI)로 올바르게 분류됩니다.

구현

Android 9 이하에서는 디스플레이가 32비트 ID로 식별되었습니다. 여기서 0은 내부 디스플레이, 1은 외부 디스플레이, [2, INT32_MAX]는 HWC 가상 디스플레이이고 -1은 잘못된 디스플레이 또는 비 HWC 가상 디스플레이를 나타냅니다.

Android 10부터는 디스플레이에 안정적이고 영구적인 ID가 주어져 SurfaceFlinger 및 DisplayManagerService 서비스가 2개 이상의 디스플레이를 추적하고 이전에 본 디스플레이를 인지할 수 있습니다. HWC가 IComposerClient.getDisplayIdentificationData를 지원하고 디스플레이 식별 데이터를 제공하는 경우 SurfaceFlinger는 EDID 구조를 파싱하고 실제 및 HWC 가상 디스플레이를 위한 안정적인 64비트 디스플레이 ID를 할당합니다. ID는 옵션 유형을 사용하여 표현되며, 여기서 null 값은 잘못된 디스플레이나 비 HWC 가상 디스플레이를 나타냅니다. HWC가 지원되지 않으면 SurfaceFlinger는 실제 디스플레이 수가 2개로 제한되는 기존 동작으로 대체됩니다.

디스플레이별 포커스

여러 개의 포커스가 맞춰진 창(디스플레이당 최대 1개)을 지원하도록 Android 10을 구성하면 동시에 개별 디스플레이를 타겟팅하는 여러 입력 소스를 지원할 수 있습니다. 이는 여러 사용자가 동시에 같은 기기와 상호작용하고 Android Automotive와 같은 다른 입력 방식이나 기기를 사용하는 경우의 특수 기기 유형만을 대상으로 합니다.

이 기능은 멀티스크린 기기 또는 데스크톱과 유사한 환경에 사용되는 일반 기기에 사용 설정하지 않는 것이 좋습니다. 주된 이유는 사용자가 어떤 창에 입력 포커스가 있는지를 궁금하게 만들 수 있는 보안 우려 때문입니다.

사용자가 은행 앱에 로그인하거나 민감한 정보가 포함된 텍스트를 입력하는 등 보안 정보를 텍스트 입력 필드에 입력한다고 가정해 보겠습니다. 악성 앱은 활동을 실행하는 데 사용되고 텍스트 입력 필드를 포함하는 가상 오프스크린 디스플레이를 생성할 수 있습니다. 합법적 활동과 악의적 활동에는 포커스가 있으며 둘 다 활성 입력 표시기(깜박이는 커서)를 표시합니다.

하지만 키보드(하드웨어 또는 소프트웨어) 입력은 최상단 활동(가장 최근에 실행된 앱)에만 적용되므로 악성 앱은 숨겨진 가상 디스플레이를 생성하여 사용자 입력을 가로챌 수 있습니다. 이는 사용자가 기본 기기 디스플레이에서 소프트웨어 키보드를 사용하는 경우에도 마찬가지입니다.

com.android.internal.R.bool.config_perDisplayFocusEnabled를 사용하여 디스플레이별 포커스를 설정하세요.

호환성

문제: Android 9 이하에서는 시스템에서 한 번에 하나의 창만 포커스를 가집니다.

해결 방법: 같은 프로세스에서 두 개의 창에 포커스가 맞춰지는 드문 경우, 시스템은 Z-order가 더 높은 창에만 포커스를 제공합니다. 이러한 제한사항은 Android 10을 타겟팅하는 앱에서 제거됩니다. Android 10부터는 동시에 포커스가 맞춰지고 있는 여러 개의 창을 지원할 수 있을 것으로 예상됩니다.

구현

WindowManagerService#mPerDisplayFocusEnabled는 이 기능의 사용 가능 여부를 제어합니다. ActivityManager에서는 이제 변수의 전역 추적 대신 ActivityDisplay#getFocusedStack()이 사용됩니다. ActivityDisplay#getFocusedStack()은 값을 캐싱하는 대신 Z-order에 따라 포커스를 결정합니다. 이는 단일 소스, 즉 WindowManager만 활동의 Z-order를 추적해야 하기 때문입니다.

ActivityStackSupervisor#getTopDisplayFocusedStack()은 시스템 최상단에 포커스가 맞춰진 스택을 식별해야 하는 경우 이와 유사한 접근 방식을 취합니다. 스택은 상단에서 하단으로 순회하며 사용 가능한 첫 번째 스택을 검색합니다.

이제 InputDispatcher에 여러 개의 포커스가 맞춰진 창을 포함할 수 있습니다(디스플레이당 하나). 입력 이벤트가 디스플레이와 관련된 경우에는 상응하는 디스플레이의 포커스가 맞춰진 창으로 전달됩니다. 관련이 없는 경우에는 포커스가 맞춰진 디스플레이, 즉 사용자가 가장 최근에 상호작용한 디스플레이의 포커스가 맞춰진 창으로 전달됩니다.

InputDispatcher::mFocusedWindowHandlesByDisplayInputDispatcher::setFocusedDisplay()를 참고하세요. 포커스가 맞춰진 앱 역시 NativeInputManager::setFocusedApplication()을 통해 InputManagerService에서 별도로 업데이트됩니다.

WindowManager에서는 포커스가 맞춰진 창도 별도로 추적됩니다. DisplayContent#mCurrentFocusDisplayContent#mFocusedApp과 각각의 용도를 참고하세요. 관련 포커스 추적 및 업데이트 메서드는 WindowManagerService에서 DisplayContent로 옮겨졌습니다.