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" />
<display
name="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" />
<display
name="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<>(); @Override public 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
。