ディスプレイのサポート

ディスプレイ固有の領域に対する更新内容は次のとおりです。

アクティビティとディスプレイのサイズ変更

アプリがマルチウィンドウ モードまたはサイズ変更をサポートしていないことを示すには、アクティビティで 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 に設定されている場合、SizeCompatModeActivityController(システム UI)はコールバックを受信してプロセス再起動ボタンを表示します。

ディスプレイ サイズとアスペクト比

Android 10 は、比率の高い細長い画面から 1 対 1 までの新しいアスペクト比に対応しています。アプリは、処理できる画面の ApplicationInfo#maxAspectRatioApplicationInfo#minAspectRatio を定義できます。

Android 10 のアプリ アスペクト比

図 1. Android 10 でサポートされるアプリのアスペクト比の例

デバイスの実装では、Android 9 以前の要件よりもサイズと解像度が小さい(幅または高さが最小で 2.5 インチ、smallestScreenWidth が最小で 320 DP)セカンダリ ディスプレイを使用できますが、表示できるのは、このような小さなディスプレイのサポートを有効にしているアクティビティのみです。

アプリでこのサポートを有効にするには、対象ディスプレイのサイズ以下のサイズを最小サポートサイズとして宣言します。この場合は、AndroidManifest で android:minHeightandroid:minWidth のアクティビティ レイアウト属性を使用します。

ディスプレイ ポリシー

Android 10 では、特定のディスプレイ ポリシーが PhoneWindowManager のデフォルトの WindowManagerPolicy 実装から分離され、次のようなディスプレイごとのクラスに移動されています。

  • ディスプレイの状態と回転
  • 一部のキーとモーション イベントのトラッキング
  • システム UI とデコレーション ウィンドウ

Android 9 以前では、ディスプレイ ポリシー、状態と設定、回転、デコレーション ウィンドウ フレーム トラッキングなどが PhoneWindowManager クラスで処理されました。Android 10 では、DisplayRotation に移動された回転トラッキング以外は DisplayPolicy クラスに移動されています。

ディスプレイのウィンドウ設定

Android 10 では、ディスプレイごとに設定可能なウィンドウ設定が拡張され、次の項目が含まれるようになりました。

  • デフォルトのディスプレイ ウィンドウ モード
  • オーバースキャン値
  • ユーザー回転と回転モード
  • 適用サイズ、密度、スケーリング モード
  • コンテンツ移転モード(ディスプレイが取り外された場合)
  • システム デコレーションと IME のサポート

DisplayWindowSettings クラスには、これらの項目の設定が含まれています。設定が変更されるたびに、ディスクの /data パーティションの display_settings.xml に保存されます。詳しくは、DisplayWindowSettings.AtomicFileStorageDisplayWindowSettings#writeSettings() をご覧ください。デバイス メーカーは、デバイス設定のデフォルト値を display_settings.xml で提供できます。ただし、このファイルは /data に保存されるため、ワイプによって消去されたファイルを復元するために追加のロジックが必要になることがあります。

Android 10 では、設定を保持する際のディスプレイの ID としてデフォルトで DisplayInfo#uniqueId が使用されます。すべてのディスプレイについて uniqueId を入力する必要があります。また、物理ディスプレイとネットワーク ディスプレイについても同様です。ID として物理ディスプレイのポートを使用することもできます。これは DisplayWindowSettings#mIdentifier で設定できます。書き込み時には毎回、すべての設定が書き込まれるため、ストレージ内のディスプレイ エントリに使用されるキーは確実に更新されます。詳しくは、静的ディスプレイ ID をご覧ください。

過去の経緯から、設定は /data ディレクトリに保持されます。もともとは、ディスプレイの回転などのユーザー設定を保持するために使用されていました。

静的ディスプレイ ID

Android 9 以前では、フレームワーク内のディスプレイに固定 ID が提供されませんでした。ディスプレイがシステムに追加されると、静的カウンタを増加させることによりそのディスプレイに対して Display#mDisplayId または DisplayInfo#displayId が生成されました。システムに同じディスプレイを追加・削除するたびに、異なる ID が使用されました。

デバイスの起動時から複数のディスプレイを使用できる場合は、タイミングに応じて各ディスプレイに異なる ID が割り当てられました。Android 9 以前にも DisplayInfo#uniqueId はありましたが、ディスプレイを区別するための十分な情報が含まれていませんでした。物理ディスプレイは local:0 または local:1(組み込みディスプレイまたは外部ディスプレイ)のいずれかとして識別されるのみでした。

Android 10 では固定 ID を追加し、ローカル ディスプレイ、ネットワーク ディスプレイ、仮想ディスプレイを区別できるように DisplayInfo#uniqueId が変更されました。

ディスプレイの種類 形式
ローカル

local:<stable-id>
ネットワーク

network:<mac-address>
仮想

virtual:<package-name-and-name>

uniqueId の更新に加え、DisplayInfo.address には再起動後も変わらないディスプレイ ID である DisplayAddress が含まれています。Android 10 では、DisplayAddress は物理ディスプレイとネットワーク ディスプレイをサポートしています。DisplayAddress.Physical は固定ディスプレイ ID(uniqueId 内のものと同じ)を含み、DisplayAddress#fromPhysicalDisplayId() を使用して作成できます。

Android 10 には、ポート情報を取得するための便利なメソッドも用意されています(Physical#getPort())。このメソッドをフレームワークで使用して、ディスプレイを静的に識別できます。たとえば DisplayWindowSettings で使用します。DisplayAddress.Network は MAC アドレスを含み、DisplayAddress#fromMacAddress() で作成できます。

上記の追加により、デバイス メーカーは物理ディスプレイのポートなどの静的ディスプレイ ID を使用して、静的なマルチディスプレイ構成内でディスプレイを識別し、さまざまなシステム設定や機能設定を行えるようになります。これらのメソッドは非表示になっており、system_server での使用のみを目的としています。

HWC ディスプレイ ID(不透明で固定されていない可能性がある)が指定されている場合、このメソッドによって、ディスプレイ出力の物理コネクタを識別する(プラットフォーム固有の)8 ビットのポート番号と、ディスプレイの EDID blob が返されます。 SurfaceFlinger は、EDID からメーカーまたはモデルの情報を抽出して、フレームワークに公開される固定の 64 ビット ディスプレイ ID を生成します。このメソッドがサポートされていない、またはエラーが発生した場合、SurfaceFlinger は DisplayInfo#address が null で DisplayInfo#uniqueId がハードコードされている(前述)、以前の MD モードにフォールバックします。

この機能がサポートされていることを確認するには、以下を実行します。

$ 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 と ID 1 を持つ物理ディスプレイが最大 2 台であることを前提としていました。

Android 10 以降では、SurfaceFlinger が Hardware Composer(HWC)API を利用して固定ディスプレイ ID を生成し、任意の数の物理ディスプレイを管理できます。詳細については、静的ディスプレイ 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 と永続 ID が付与されます。これにより、SurfaceFlinger と DisplayManagerService は 3 台以上のディスプレイをトラッキングして、以前に接続したディスプレイを認識できます。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 以前のシステムでは、一度に 1 つのウィンドウのみフォーカスされます。

解決策: まれに同じプロセスの 2 つのウィンドウがフォーカスされる場合、Z オーダーの高いウィンドウのみフォーカスされます。この制限は、Android 10 をターゲットとするアプリでは削除されるため、同時にフォーカスされる複数のウィンドウに対応できることが想定されています。

実装

WindowManagerService#mPerDisplayFocusEnabled はこの機能の可用性を制御します。ActivityManagerActivityDisplay#getFocusedStack() は、変数でのグローバル トラッキングの代わりに使用されます。ActivityDisplay#getFocusedStack() は、値をキャッシュする代わりに Z オーダーに基づいてフォーカスを決定します。つまり、アクティビティの Z オーダーをトラッキングする必要があるのは 1 つのソース(WindowManager)のみです。

ActivityStackSupervisor#getTopDisplayFocusedStack() は、システムでフォーカスされる最上位のスタックを特定する必要がある場合に同様の方法を使用します。スタックが上から下に走査され、最初の有効なスタックが検索されます。

InputDispatcher は、複数のフォーカスされたウィンドウ(ディスプレイごとに 1 つ)を対象にできるようになりました。ディスプレイ固有の入力イベントは、対応するディスプレイのフォーカスされたウィンドウにディスパッチされます。他の入力イベントは、フォーカスされたディスプレイ(ユーザーが最後に操作したディスプレイ)のフォーカスされたウィンドウにディスパッチされます。

InputDispatcher::mFocusedWindowHandlesByDisplayInputDispatcher::setFocusedDisplay() をご覧ください。NativeInputManager::setFocusedApplication() によって、フォーカスされたアプリも InputManagerService で個別に更新されます。

WindowManager では、フォーカスされたウィンドウも個別にトラッキングされます。 DisplayContent#mCurrentFocusDisplayContent#mFocusedApp、およびそれぞれの使用法をご覧ください。関連するフォーカスのトラッキングと更新のメソッドが WindowManagerService から DisplayContent に移動されました。