强制使用桌面模式 - SECONDARY_HOME -android-15.0.0_r23
- [1. 设置层:UI 开关与全局数据库写入](#1. 设置层:UI 开关与全局数据库写入)
- [2. Framework 层](#2. Framework 层)
-
- [2.1 DisplayContent 中 isHomeSupported()](#2.1 DisplayContent 中 isHomeSupported())
- [2.2 RootWindowContainer 在副屏启动SECONDARY_HOME桌面](#2.2 RootWindowContainer 在副屏启动SECONDARY_HOME桌面)
- [3. SECONDARY_HOME的桌面Activity](#3. SECONDARY_HOME的桌面Activity)

1. 设置层:UI 开关与全局数据库写入
"开发者选项" > "强制使用桌面模式" 开关
packages/apps/Settings/res/xml/development_settings.xml
packages/apps/Settings/src/com/android/settings/development/DesktopModeSecondaryDisplayPreferenceController.java
- UI 入口:在
Settings应用development_settings.xml布局中force_desktop_mode_on_external_displays - 数据库写入:开关状态被写入 Settings.Global 表,
public static final String DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS = "force_desktop_mode_on_external_displays";
java
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean isEnabled = (Boolean) newValue;
Settings.Global.putInt(mContext.getContentResolver(),
DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
// Update freeform window support on device.
// DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT setting enables freeform support on device
// where it's not present by default.
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT,
isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
if (isEnabled && mFragment != null) {
RebootConfirmationDialogFragment.show(
mFragment, R.string.reboot_dialog_enable_desktop_mode_on_secondary_display,
this);
}
return true;
}
2. Framework 层
2.1 DisplayContent 中 isHomeSupported()
DisplayContent中isHomeSupported():
mWmService.mDisplayWindowSettings.isHomeSupportedLocked(this)其中settings.mShouldShowSystemDecors添加有display_settings.xml文件配置属性shouldShowSystemDecors="false"isTrusted()在LocalDisplayAdapter.java中getDisplayDeviceInfoLocked()获取mInfo.flags |= DisplayDeviceInfo.FLAG_TRUSTED;isSystemDecorationsSupported()调用isSystemDecorationsSupported() - isPublicSecondaryDisplayWithDesktopModeForceEnabled()中mWmService.mForceDesktopModeOnExternalDisplays对应mForceDesktopModeOnExternalDisplays = Settings.Global.getInt(resolver, DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0;
frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java
java
@Nullable ComponentName getCustomHomeComponent() {
if (!isHomeSupported() || mDwpcHelper == null) {
return null;
}
return mDwpcHelper.getCustomHomeComponent();
}
boolean isSystemDecorationsSupported() {
if (mDisplayId == mWmService.mVr2dDisplayId) {
// VR virtual display will be used to run and render 2D app within a VR experience.
return false;
}
if (!isTrusted()) {
// Do not show system decorations on untrusted virtual display.
return false;
}
if (mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(this)
|| (mDisplay.getFlags() & FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS) != 0) {
// This display is configured to show system decorations.
return true;
}
if (isPublicSecondaryDisplayWithDesktopModeForceEnabled()) {
if (com.android.window.flags.Flags.rearDisplayDisableForceDesktopSystemDecorations()) {
// System decorations should not be forced on a rear display due to security
// policies.
return (mDisplay.getFlags() & Display.FLAG_REAR) == 0;
}
// If the display is forced to desktop mode, treat it the same as it is configured to
// show system decorations.
return true;
}
return false;
}
/**
* This is the development option to force enable desktop mode on all secondary public displays
* that are not owned by a virtual device.
* When this is enabled, it also force enable system decorations on those displays.
*
* If we need a per-display config to enable desktop mode for production, that config should
* also check {@link #isSystemDecorationsSupported()} to avoid breaking any security policy.
*/
boolean isPublicSecondaryDisplayWithDesktopModeForceEnabled() {
if (!mWmService.mForceDesktopModeOnExternalDisplays || isDefaultDisplay || isPrivate()) {
return false;
}
// Desktop mode is not supported on virtual devices.
int deviceId = mRootWindowContainer.mTaskSupervisor.getDeviceIdForDisplayId(mDisplayId);
return deviceId == Context.DEVICE_ID_DEFAULT;
}
/**
* Checks if this display is configured and allowed to show home activity and wallpaper.
*
* <p>This is implied for displays that have {@link Display#FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS}
* and can also be set via {@link VirtualDisplayConfig.Builder#setHomeSupported}.</p>
*/
boolean isHomeSupported() {
return (mWmService.mDisplayWindowSettings.isHomeSupportedLocked(this) && isTrusted())
|| isSystemDecorationsSupported();
}
2.2 RootWindowContainer 在副屏启动SECONDARY_HOME桌面
startHomeOnTaskDisplayArea:
shouldPlaceSecondaryHomeOnDisplayArea最终会判断display.isHomeSupported()是否支持resolveSecondaryHomeActivity中调用resolveSecondaryHomeActivity获取getSecondaryHomeIntent包含Intent.CATEGORY_SECONDARY_HOME的桌面
frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java
java
boolean startHomeOnTaskDisplayArea(int userId, String reason, TaskDisplayArea taskDisplayArea,
boolean allowInstrumenting, boolean fromHomeKey) {
// Fallback to top focused display area if the provided one is invalid.
if (taskDisplayArea == null) {
final Task rootTask = getTopDisplayFocusedRootTask();
taskDisplayArea = rootTask != null ? rootTask.getDisplayArea()
: getDefaultTaskDisplayArea();
}
Intent homeIntent = null;
ActivityInfo aInfo = null;
if (taskDisplayArea == getDefaultTaskDisplayArea()
|| mWmService.shouldPlacePrimaryHomeOnDisplay(
taskDisplayArea.getDisplayId(), userId)) {
homeIntent = mService.getHomeIntent();
aInfo = resolveHomeActivity(userId, homeIntent);
} else if (shouldPlaceSecondaryHomeOnDisplayArea(taskDisplayArea)) {
Pair<ActivityInfo, Intent> info = resolveSecondaryHomeActivity(userId, taskDisplayArea);
aInfo = info.first;
homeIntent = info.second;
}
if (aInfo == null || homeIntent == null) {
return false;
}
if (!canStartHomeOnDisplayArea(aInfo, taskDisplayArea, allowInstrumenting)) {
return false;
}
if (mService.mAmInternal.shouldDelayHomeLaunch(userId)) {
Slog.d(TAG, "ThemeHomeDelay: Home launch was deferred with user " + userId);
return false;
}
// Updates the home component of the intent.
homeIntent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
homeIntent.setFlags(homeIntent.getFlags() | FLAG_ACTIVITY_NEW_TASK);
// Updates the extra information of the intent.
if (fromHomeKey) {
homeIntent.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, true);
}
homeIntent.putExtra(WindowManagerPolicy.EXTRA_START_REASON, reason);
// Update the reason for ANR debugging to verify if the user activity is the one that
// actually launched.
final String myReason = reason + ":" + userId + ":" + UserHandle.getUserId(
aInfo.applicationInfo.uid) + ":" + taskDisplayArea.getDisplayId();
mService.getActivityStartController().startHomeActivity(homeIntent, aInfo, myReason,
taskDisplayArea);
return true;
}
/**
* This resolves the home activity info.
*
* @return the home activity info if any.
*/
@VisibleForTesting
ActivityInfo resolveHomeActivity(int userId, Intent homeIntent) {
final int flags = ActivityManagerService.STOCK_PM_FLAGS;
final ComponentName comp = homeIntent.getComponent();
ActivityInfo aInfo = null;
try {
if (comp != null) {
// Factory test.
aInfo = AppGlobals.getPackageManager().getActivityInfo(comp, flags, userId);
} else {
final String resolvedType =
homeIntent.resolveTypeIfNeeded(mService.mContext.getContentResolver());
final ResolveInfo info = mTaskSupervisor.resolveIntent(homeIntent, resolvedType,
userId, flags, Binder.getCallingUid(), Binder.getCallingPid());
if (info != null) {
aInfo = info.activityInfo;
}
}
} catch (RemoteException e) {
// ignore
}
if (aInfo == null) {
Slogf.wtf(TAG, new Exception(), "No home screen found for %s and user %d", homeIntent,
userId);
return null;
}
aInfo = new ActivityInfo(aInfo);
aInfo.applicationInfo = mService.getAppInfoForUser(aInfo.applicationInfo, userId);
return aInfo;
}
@VisibleForTesting
Pair<ActivityInfo, Intent> resolveSecondaryHomeActivity(int userId,
@NonNull TaskDisplayArea taskDisplayArea) {
if (taskDisplayArea == getDefaultTaskDisplayArea()) {
throw new IllegalArgumentException(
"resolveSecondaryHomeActivity: Should not be default task container");
}
Intent homeIntent = mService.getHomeIntent();
ActivityInfo aInfo = resolveHomeActivity(userId, homeIntent);
boolean lookForSecondaryHomeActivityInPrimaryHomePackage = aInfo != null;
if (android.companion.virtual.flags.Flags.vdmCustomHome()) {
// Resolve the externally set home activity for this display, if any. If it is unset or
// we fail to resolve it, fallback to the default secondary home activity.
final ComponentName customHomeComponent =
taskDisplayArea.getDisplayContent() != null
? taskDisplayArea.getDisplayContent().getCustomHomeComponent()
: null;
if (customHomeComponent != null) {
homeIntent.setComponent(customHomeComponent);
ActivityInfo customHomeActivityInfo = resolveHomeActivity(userId, homeIntent);
if (customHomeActivityInfo != null) {
aInfo = customHomeActivityInfo;
lookForSecondaryHomeActivityInPrimaryHomePackage = false;
}
}
}
if (lookForSecondaryHomeActivityInPrimaryHomePackage) {
// Resolve activities in the same package as currently selected primary home activity.
if (ResolverActivity.class.getName().equals(aInfo.name)) {
// Always fallback to secondary home component if default home is not set.
aInfo = null;
} else {
// Look for secondary home activities in the currently selected default home
// package.
homeIntent = mService.getSecondaryHomeIntent(aInfo.applicationInfo.packageName);
final List<ResolveInfo> resolutions = resolveActivities(userId, homeIntent);
final int size = resolutions.size();
final String targetName = aInfo.name;
aInfo = null;
for (int i = 0; i < size; i++) {
ResolveInfo resolveInfo = resolutions.get(i);
// We need to traverse all resolutions to check if the currently selected
// default home activity is present.
if (resolveInfo.activityInfo.name.equals(targetName)) {
aInfo = resolveInfo.activityInfo;
break;
}
}
if (aInfo == null && size > 0) {
// First one is the best.
aInfo = resolutions.get(0).activityInfo;
}
}
}
if (aInfo != null) {
if (!canStartHomeOnDisplayArea(aInfo, taskDisplayArea,
false /* allowInstrumenting */)) {
aInfo = null;
}
}
// Fallback to secondary home component.
if (aInfo == null) {
homeIntent = mService.getSecondaryHomeIntent(null);
aInfo = resolveHomeActivity(userId, homeIntent);
}
return Pair.create(aInfo, homeIntent);
}
/**
* Retrieve all activities that match the given intent.
* The list should already ordered from best to worst matched.
* {@link android.content.pm.PackageManager#queryIntentActivities}
*/
@VisibleForTesting
List<ResolveInfo> resolveActivities(int userId, Intent homeIntent) {
List<ResolveInfo> resolutions;
try {
final String resolvedType =
homeIntent.resolveTypeIfNeeded(mService.mContext.getContentResolver());
resolutions = AppGlobals.getPackageManager().queryIntentActivities(homeIntent,
resolvedType, ActivityManagerService.STOCK_PM_FLAGS, userId).getList();
} catch (RemoteException e) {
resolutions = new ArrayList<>();
}
return resolutions;
}
boolean resumeHomeActivity(ActivityRecord prev, String reason,
TaskDisplayArea taskDisplayArea) {
if (!mService.isBooting() && !mService.isBooted()) {
// Not ready yet!
return false;
}
if (taskDisplayArea == null) {
taskDisplayArea = getDefaultTaskDisplayArea();
}
final ActivityRecord r = taskDisplayArea.getHomeActivity();
final String myReason = reason + " resumeHomeActivity";
// Only resume home activity if isn't finishing.
if (r != null && !r.finishing) {
r.moveFocusableActivityToTop(myReason);
return resumeFocusedTasksTopActivities(r.getRootTask(), prev);
}
int userId = mWmService.getUserAssignedToDisplay(taskDisplayArea.getDisplayId());
return startHomeOnTaskDisplayArea(userId, myReason, taskDisplayArea,
false /* allowInstrumenting */, false /* fromHomeKey */);
}
/**
* Check if the display is valid for primary home activity.
*
* @param displayId The target display ID
* @return {@code true} if allowed to launch, {@code false} otherwise.
*/
boolean shouldPlacePrimaryHomeOnDisplay(int displayId) {
// No restrictions to default display, vr 2d display or main display for visible users.
return displayId == DEFAULT_DISPLAY || (displayId != INVALID_DISPLAY
&& (displayId == mService.mVr2dDisplayId
|| mWmService.shouldPlacePrimaryHomeOnDisplay(displayId)));
}
/**
* Check if the display area is valid for secondary home activity.
*
* @param taskDisplayArea The target display area.
* @return {@code true} if allow to launch, {@code false} otherwise.
*/
boolean shouldPlaceSecondaryHomeOnDisplayArea(TaskDisplayArea taskDisplayArea) {
if (getDefaultTaskDisplayArea() == taskDisplayArea) {
throw new IllegalArgumentException(
"shouldPlaceSecondaryHomeOnDisplay: Should not be on default task container");
} else if (taskDisplayArea == null) {
return false;
}
if (!taskDisplayArea.canHostHomeTask()) {
// Can't launch home on a TaskDisplayArea that does not support root home task
return false;
}
if (taskDisplayArea.getDisplayId() != DEFAULT_DISPLAY && !mService.mSupportsMultiDisplay) {
// Can't launch home on secondary display if device does not support multi-display.
return false;
}
final boolean deviceProvisioned = Settings.Global.getInt(
mService.mContext.getContentResolver(),
Settings.Global.DEVICE_PROVISIONED, 0) != 0;
if (!deviceProvisioned) {
// Can't launch home on secondary display areas before device is provisioned.
return false;
}
if (!StorageManager.isCeStorageUnlocked(mCurrentUser)) {
// Can't launch home on secondary display areas if CE storage is still locked.
return false;
}
final DisplayContent display = taskDisplayArea.getDisplayContent();
if (display == null || display.isRemoved() || !display.isHomeSupported()) {
// Can't launch home on display that doesn't support home.
return false;
}
return true;
}
/**
* Check if home activity start should be allowed on a {@link TaskDisplayArea}.
*
* @param homeInfo {@code ActivityInfo} of the home activity that is going to be
* launched.
* @param taskDisplayArea The target display area.
* @param allowInstrumenting Whether launching home should be allowed if being instrumented.
* @return {@code true} if allow to launch, {@code false} otherwise.
*/
boolean canStartHomeOnDisplayArea(ActivityInfo homeInfo, TaskDisplayArea taskDisplayArea,
boolean allowInstrumenting) {
if (mService.mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL
&& mService.mTopAction == null) {
// We are running in factory test mode, but unable to find the factory test app, so
// just sit around displaying the error message and don't try to start anything.
return false;
}
final WindowProcessController app =
mService.getProcessController(homeInfo.processName, homeInfo.applicationInfo.uid);
if (!allowInstrumenting && app != null && app.isInstrumenting()) {
// Don't do this if the home app is currently being instrumented.
return false;
}
if (taskDisplayArea != null && !taskDisplayArea.canHostHomeTask()) {
return false;
}
final int displayId = taskDisplayArea != null ? taskDisplayArea.getDisplayId()
: INVALID_DISPLAY;
if (shouldPlacePrimaryHomeOnDisplay(displayId)) {
return true;
}
if (!shouldPlaceSecondaryHomeOnDisplayArea(taskDisplayArea)) {
return false;
}
final boolean supportMultipleInstance = homeInfo.launchMode != LAUNCH_SINGLE_TASK
&& homeInfo.launchMode != LAUNCH_SINGLE_INSTANCE;
if (!supportMultipleInstance) {
// Can't launch home on secondary displays if it requested to be single instance.
return false;
}
return true;
}
frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
java
/**
* Return the intent set with {@link Intent#CATEGORY_SECONDARY_HOME} to resolve secondary home
* activities.
*
* @param preferredPackage Specify a preferred package name, otherwise use the package name
* defined in config_secondaryHomePackage.
* @return the intent set with {@link Intent#CATEGORY_SECONDARY_HOME}
*/
Intent getSecondaryHomeIntent(String preferredPackage) {
final Intent intent = new Intent(mTopAction, mTopData != null ? Uri.parse(mTopData) : null);
final boolean useSystemProvidedLauncher = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_useSystemProvidedLauncherForSecondary);
if (preferredPackage == null || useSystemProvidedLauncher) {
// Using the package name stored in config if no preferred package name or forced.
final String secondaryHomePackage = mContext.getResources().getString(
com.android.internal.R.string.config_secondaryHomePackage);
intent.setPackage(secondaryHomePackage);
} else {
intent.setPackage(preferredPackage);
}
intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
if (mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
intent.addCategory(Intent.CATEGORY_SECONDARY_HOME);
}
return intent;
}
3. SECONDARY_HOME的桌面Activity
packages/apps/Launcher3/AndroidManifest-common.xml
xml
<!--
Launcher activity for secondary display
-->
<activity
android:name="com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher"
android:theme="@style/AppTheme"
android:launchMode="singleTop"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.SECONDARY_HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>