Android多屏幕支持-Android12
- 1、概覽及相關文章
- 2、屏幕窗口配置
- 2.1 配置xml文件
- 2.2 DisplayInfo#uniqueId 屏幕標識
- 2.3 adb查看信息
- 3、配置文件解析
- 3.1 xml字段讀取
- 3.2 簡要時序圖
- 4、每屏幕焦點
android12-release
1、概覽及相關文章
AOSP > 文檔 > 心主題 > 多屏幕概覽
術語
在這些文章中,主屏幕和輔助屏幕的定義如下:主(默認)屏幕的
屏幕 ID
為DEFAULT_DISPLAY
輔助屏幕的屏幕 ID
不是DEFAULT_DISPLAY
主題區域 | 文章 |
---|---|
開發和測試 | 推薦做法 測試和開發環境 常見問題解答 |
相關文章集 | 顯示 系統裝飾支持 輸入法支持 |
單篇文章 | 多項恢復 Activity 啟動政策 鎖定屏幕 輸入路由 多區音頻 |
2、屏幕窗口配置
2.1 配置xml文件
/data/system/display_settings.xml
配置:
- 模擬屏幕:
uniqueId
用于在名稱屬性中標識屏幕,對于模擬屏幕,此 ID 為overlay:1
。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<display-settings>
<config identifier="0" />
<displayname="overlay:1"shouldShowSystemDecors="true"shouldShowIme="true" />
</display-settings>
- 內置屏幕:
uniqueId
對于內置屏幕,示例值可以是 “local:45354385242535243453”。另一種方式是使用硬件端口信息,并設置 identifier=“1” 以與 DisplayWindowSettingsProvider#IDENTIFIER_PORT 對應,然后更新 name 以使用"port:<port_id>" 格式
。
<?xmlversion='1.0' encoding='utf-8' standalone='yes' ?>
<display-settings>
<config identifier="1" />
<displayname="port:12345"shouldShowSystemDecors="true"shouldShowIme="true" />
</display-settings>
2.2 DisplayInfo#uniqueId 屏幕標識
DisplayInfo#uniqueId,以添加穩定的標識符并區分本地、網絡和虛擬屏幕
屏幕類型 | 格式 |
---|---|
本地 | local:<stable-id> |
網絡 | network:<mac-address> |
虛擬 | virtual:<package-name-and-name> |
2.3 adb查看信息
$ 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"
frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp#SurfaceFlinger::dumpDisplayIdentificationData
void SurfaceFlinger::dumpDisplayIdentificationData(std::string& result) const {for (const auto& [token, display] : mDisplays) {const auto displayId = PhysicalDisplayId::tryCast(display->getId());if (!displayId) {continue;}const auto hwcDisplayId = getHwComposer().fromPhysicalDisplayId(*displayId);if (!hwcDisplayId) {continue;}StringAppendF(&result,"Display %s (HWC display %" PRIu64 "): ", to_string(*displayId).c_str(),*hwcDisplayId);uint8_t port;DisplayIdentificationData data;if (!getHwComposer().getDisplayIdentificationData(*hwcDisplayId, &port, &data)) {result.append("no identification data\n");continue;}if (!isEdid(data)) {result.append("unknown identification data\n");continue;}const auto edid = parseEdid(data);if (!edid) {result.append("invalid EDID\n");continue;}StringAppendF(&result, "port=%u pnpId=%s displayName=\"", port, edid->pnpId.data());result.append(edid->displayName.data(), edid->displayName.length());result.append("\"\n");}
}
3、配置文件解析
3.1 xml字段讀取
- 文件路徑:
DATA_DISPLAY_SETTINGS_FILE_PATH = "system/display_settings.xml"
、VENDOR_DISPLAY_SETTINGS_FILE_PATH = "etc/display_settings.xml"
、Settings.Global.getString(resolver,DEVELOPMENT_WM_DISPLAY_SETTINGS_PATH)
(DEVELOPMENT_WM_DISPLAY_SETTINGS_PATH = "wm_display_settings_path"
)FileData
對象:fileData.mIdentifierType = getIntAttribute(parser, "identifier", IDENTIFIER_UNIQUE_ID)
、name = parser.getAttributeValue(null, "name")
、shouldShowIme = getBooleanAttribute(parser, "shouldShowIme", null /* defaultValue */)
、settingsEntry.mShouldShowSystemDecors = getBooleanAttribute(parser, "shouldShowSystemDecors", null /* defaultValue */)
等等private static final class FileData {int mIdentifierType;final Map<String, SettingsEntry> mSettings = new HashMap<>();@Overridepublic String toString() {return "FileData{"+ "mIdentifierType=" + mIdentifierType+ ", mSettings=" + mSettings+ '}';} }
DisplayWindowSettings.java
有關顯示器的當前持久設置。提供顯示設置的策略,并將設置值的持久性和查找委派給提供的{@link SettingsProvider}
@Nullable
private static FileData readSettings(ReadableSettingsStorage storage) {InputStream stream;try {stream = storage.openRead();} catch (IOException e) {Slog.i(TAG, "No existing display settings, starting empty");return null;}FileData fileData = new FileData();boolean success = false;try {TypedXmlPullParser parser = Xml.resolvePullParser(stream);int type;while ((type = parser.next()) != XmlPullParser.START_TAG&& type != XmlPullParser.END_DOCUMENT) {// Do nothing.}if (type != XmlPullParser.START_TAG) {throw new IllegalStateException("no start tag found");}int outerDepth = parser.getDepth();while ((type = parser.next()) != XmlPullParser.END_DOCUMENT&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {continue;}String tagName = parser.getName();if (tagName.equals("display")) {readDisplay(parser, fileData);} else if (tagName.equals("config")) {readConfig(parser, fileData);} else {Slog.w(TAG, "Unknown element under <display-settings>: "+ parser.getName());XmlUtils.skipCurrentTag(parser);}}success = true;} catch (IllegalStateException e) {Slog.w(TAG, "Failed parsing " + e);} catch (NullPointerException e) {Slog.w(TAG, "Failed parsing " + e);} catch (NumberFormatException e) {Slog.w(TAG, "Failed parsing " + e);} catch (XmlPullParserException e) {Slog.w(TAG, "Failed parsing " + e);} catch (IOException e) {Slog.w(TAG, "Failed parsing " + e);} catch (IndexOutOfBoundsException e) {Slog.w(TAG, "Failed parsing " + e);} finally {try {stream.close();} catch (IOException ignored) {}}if (!success) {fileData.mSettings.clear();}return fileData;
}private static int getIntAttribute(TypedXmlPullParser parser, String name, int defaultValue) {return parser.getAttributeInt(null, name, defaultValue);
}@Nullable
private static Integer getIntegerAttribute(TypedXmlPullParser parser, String name,@Nullable Integer defaultValue) {try {return parser.getAttributeInt(null, name);} catch (Exception ignored) {return defaultValue;}
}@Nullable
private static Boolean getBooleanAttribute(TypedXmlPullParser parser, String name,@Nullable Boolean defaultValue) {try {return parser.getAttributeBoolean(null, name);} catch (Exception ignored) {return defaultValue;}
}private static void readDisplay(TypedXmlPullParser parser, FileData fileData)throws NumberFormatException, XmlPullParserException, IOException {String name = parser.getAttributeValue(null, "name");if (name != null) {SettingsEntry settingsEntry = new SettingsEntry();settingsEntry.mWindowingMode = getIntAttribute(parser, "windowingMode",WindowConfiguration.WINDOWING_MODE_UNDEFINED /* defaultValue */);settingsEntry.mUserRotationMode = getIntegerAttribute(parser, "userRotationMode",null /* defaultValue */);settingsEntry.mUserRotation = getIntegerAttribute(parser, "userRotation",null /* defaultValue */);settingsEntry.mForcedWidth = getIntAttribute(parser, "forcedWidth",0 /* defaultValue */);settingsEntry.mForcedHeight = getIntAttribute(parser, "forcedHeight",0 /* defaultValue */);settingsEntry.mForcedDensity = getIntAttribute(parser, "forcedDensity",0 /* defaultValue */);settingsEntry.mForcedScalingMode = getIntegerAttribute(parser, "forcedScalingMode",null /* defaultValue */);settingsEntry.mRemoveContentMode = getIntAttribute(parser, "removeContentMode",REMOVE_CONTENT_MODE_UNDEFINED /* defaultValue */);settingsEntry.mShouldShowWithInsecureKeyguard = getBooleanAttribute(parser,"shouldShowWithInsecureKeyguard", null /* defaultValue */);settingsEntry.mShouldShowSystemDecors = getBooleanAttribute(parser,"shouldShowSystemDecors", null /* defaultValue */);final Boolean shouldShowIme = getBooleanAttribute(parser, "shouldShowIme",null /* defaultValue */);if (shouldShowIme != null) {settingsEntry.mImePolicy = shouldShowIme ? DISPLAY_IME_POLICY_LOCAL: DISPLAY_IME_POLICY_FALLBACK_DISPLAY;} else {settingsEntry.mImePolicy = getIntegerAttribute(parser, "imePolicy",null /* defaultValue */);}settingsEntry.mFixedToUserRotation = getIntegerAttribute(parser, "fixedToUserRotation",null /* defaultValue */);settingsEntry.mIgnoreOrientationRequest = getBooleanAttribute(parser,"ignoreOrientationRequest", null /* defaultValue */);settingsEntry.mIgnoreDisplayCutout = getBooleanAttribute(parser,"ignoreDisplayCutout", null /* defaultValue */);settingsEntry.mDontMoveToTop = getBooleanAttribute(parser,"dontMoveToTop", null /* defaultValue */);fileData.mSettings.put(name, settingsEntry);}XmlUtils.skipCurrentTag(parser);
}private static void readConfig(TypedXmlPullParser parser, FileData fileData)throws NumberFormatException,XmlPullParserException, IOException {fileData.mIdentifierType = getIntAttribute(parser, "identifier",IDENTIFIER_UNIQUE_ID);XmlUtils.skipCurrentTag(parser);
}
3.2 簡要時序圖
4、每屏幕焦點
每個屏幕焦點
為了同時支持多個以單個屏幕為目標的輸入源,可以將 Android 10 配置為支持多個聚焦窗口,每個屏幕最多支持一個。當多個用戶同時與同一設備交互并使用不同的輸入方法或設備(例如 Android Automotive)時,此功能僅適用于特殊類型的設備。
強烈建議不要為常規設備啟用此功能,包括跨屏設備或用于類似桌面設備體驗的設備。這主要是出于安全方面的考慮,因為這樣做可能會導致用戶不確定哪個窗口具有輸入焦點
。
想象一下,用戶在文本輸入字段中輸入安全信息,也許是登錄某個銀行應用或者輸入包含敏感信息的文本。惡意應用可以創建一個虛擬的屏幕外屏幕用于執行 activity,也可以使用文本輸入字段執行 activity。合法 activity 和惡意 activity 均具有焦點,并且都顯示一個有效的輸入指示符(閃爍光標)。
不過,鍵盤(硬件或軟件)的輸入只能進入最頂層的 activity(最近啟動的應用)。通過創建隱藏的虛擬屏幕,即使在主設備屏幕上使用軟件鍵盤,惡意應用也可以獲取用戶輸入。
使用
com.android.internal.R.bool.config_perDisplayFocusEnabled
設置每屏幕焦點。
兼容性
**問題:**在 Android 9 及更低版本中,系統中一次最多只有一個窗口具有焦點。**解決方案:**在極少數情況下,來自同一進程的兩個窗口都處于聚焦狀態,則系統僅向在 Z 軸順序中較高的窗口提供焦點。對于以 Android 10 為目標平臺的應用,目前已取消這一限制,此時預計這些應用可以支持同時聚焦多個窗口。
實現
WindowManagerService#mPerDisplayFocusEnabled
用于控制此功能的可用性。在ActivityManager
中,系統現在使用的是ActivityDisplay#getFocusedStack()
,而不是利用變量進行全局跟蹤。ActivityDisplay#getFocusedStack()
根據 Z 軸順序確定焦點,而不是通過緩存值來確定。這樣一來,只有一個來源WindowManager
需要跟蹤 activity 的 Z 軸順序。如果必須要確定系統中最頂層的聚焦堆棧,
ActivityStackSupervisor#getTopDisplayFocusedStack()
會采用類似的方法處理這些情況。系統將從上到下遍歷這些堆棧,搜索第一個符合條件的堆棧。
InputDispatcher
現在可以有多個聚焦窗口(每個屏幕一個)。如果某個輸入事件特定于屏幕,則該事件會被分派到相應屏幕中的聚焦窗口。否則,它會被分派到聚焦屏幕(即用戶最近與之交互的屏幕)中的聚焦窗口。請參閱
InputDispatcher::mFocusedWindowHandlesByDisplay 和 InputDispatcher::setFocusedDisplay()
。聚焦應用也會通過NativeInputManager::setFocusedApplication()
在InputManagerService
中分別更新。在
WindowManager
中,系統還會單獨跟蹤聚焦窗口。請參閱DisplayContent#mCurrentFocus
和DisplayContent#mFocusedApp
以及各自的用途。相關的焦點跟蹤和更新方法已從WindowManagerService
移至DisplayContent
。