diff --git a/Android.bp b/Android.bp
index 0d41864e2c1d7..ab3aeecadcc61 100644
--- a/Android.bp
+++ b/Android.bp
@@ -142,6 +142,7 @@ filegroup {
":deviceproductinfoconstants_aidl",
":adbrootservice_aidl",
+ ":lmofreeform_aidl",
// For the generated R.java and Manifest.java
":framework-res{.aapt.srcjar}",
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 2174513c83e56..b149d03f351ad 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -24,17 +24,21 @@
import android.hardware.SensorManager;
import android.hardware.input.HostUsiVersion;
import android.os.Handler;
+import android.os.IBinder;
import android.os.PowerManager;
import android.util.IntArray;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
+import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceControl.RefreshRateRange;
import android.view.SurfaceControl.Transaction;
import android.window.DisplayWindowPolicyController;
import android.window.ScreenCaptureInternal;
+import com.libremobileos.freeform.ILMOFreeformDisplayCallback;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
@@ -501,6 +505,17 @@ public abstract RefreshRateRange getRefreshRateForDisplayAndSensor(
*/
public abstract IntArray getDisplayIds();
+ // LMOFreeform
+ public abstract void createFreeformLocked(String name, ILMOFreeformDisplayCallback callback,
+ int width, int height, int densityDpi, boolean secure, boolean ownContentOnly,
+ boolean shouldShowSystemDecorations, Surface surface, float refreshRate,
+ long presentationDeadlineNanos);
+
+ public abstract void resizeFreeform(IBinder appToken, int width, int height,
+ int densityDpi);
+
+ public abstract void releaseFreeform(IBinder appToken);
+
/**
* Get group id for given display id
*/
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c0e8d817b7092..be76d598a7a20 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6936,6 +6936,20 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean
*/
public static final String VIBRATE_ON_DISCONNECT = "vibrate_on_disconnect";
+ /**
+ * GameSpace: List of added games by user
+ * @hide
+ */
+ @Readable
+ public static final String GAMESPACE_GAME_LIST = "gamespace_game_list";
+
+ /**
+ * GameSpace: Whether fullscreen intent will be suppressed while in game session
+ * @hide
+ */
+ @Readable
+ public static final String GAMESPACE_SUPPRESS_FULLSCREEN_INTENT = "gamespace_suppress_fullscreen_intent";
+
/**
* Whether to enable advanced reboot
* @hide
@@ -13599,6 +13613,15 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val
*/
public static final String BERRY_BLACK_THEME = "berry_black_theme";
+ /**
+ * Our GameSpace can't write to device_config directly [GTS]
+ * Use this as intermediate to pass device_config property
+ * from our GameSpace to com.android.server.app.GameManagerService
+ * so we can set the device_config property from there.
+ * @hide
+ */
+ public static final String GAME_OVERLAY = "game_overlay";
+
/**
* User selectable PIF data.
* @hide
diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java
index dbb68372ab985..ac3a59d5472a0 100644
--- a/core/java/android/view/animation/AnimationUtils.java
+++ b/core/java/android/view/animation/AnimationUtils.java
@@ -30,13 +30,17 @@
import android.content.res.Resources.Theme;
import android.content.res.XmlResourceParser;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.ravenwood.annotation.RavenwoodIgnore;
import android.ravenwood.annotation.RavenwoodKeepPartialClass;
import android.util.AttributeSet;
+import android.util.DisplayMetrics;
import android.util.TimeUtils;
import android.util.Xml;
import android.view.InflateException;
+import com.android.internal.R;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -224,6 +228,20 @@ public static long getExpectedPresentationTimeMillis() {
public static Animation loadAnimation(Context context, @AnimRes int id)
throws NotFoundException {
+ if (SystemProperties.getBoolean("persist.sys.activity_anim_perf_override", false)) {
+ ActivityAnimations.maybeInit(context);
+ switch (id) {
+ case R.anim.activity_open_enter:
+ return ActivityAnimations.getOpenEnter();
+ case R.anim.activity_open_exit:
+ return ActivityAnimations.getOpenExit();
+ case R.anim.activity_close_enter:
+ return ActivityAnimations.getCloseEnter();
+ case R.anim.activity_close_exit:
+ return ActivityAnimations.getCloseExit();
+ }
+ }
+
XmlResourceParser parser = null;
try {
parser = context.getResources().getAnimation(id);
@@ -504,4 +522,100 @@ private static Interpolator createInterpolatorFromXml(
}
return interpolator;
}
+
+ /** @hide */
+ public final class ActivityAnimations {
+
+ private static Animation sOpenEnter;
+ private static Animation sOpenExit;
+ private static Animation sCloseEnter;
+ private static Animation sCloseExit;
+
+ private static Interpolator sFastOutExtraSlowInInterpolator;
+
+ private static final float DISTANCE = 0.1f;
+
+ private ActivityAnimations() {}
+
+ /** @hide */
+ public static void maybeInit(Context context) {
+ if (sFastOutExtraSlowInInterpolator == null) {
+ sFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
+ context, R.interpolator.fast_out_extra_slow_in);
+ }
+ }
+
+ private static class ActivityAnimFactory {
+ private float fromX = 0f, toX = 0f;
+ private long duration = 200L;
+
+ public ActivityAnimFactory fromX(float ratio) {
+ this.fromX = ratio;
+ return this;
+ }
+
+ public ActivityAnimFactory toX(float ratio) {
+ this.toX = ratio;
+ return this;
+ }
+
+ public Animation build() {
+ AnimationSet animationSet = new AnimationSet(false);
+ TranslateAnimation slide = new TranslateAnimation(
+ Animation.RELATIVE_TO_SELF, fromX,
+ Animation.RELATIVE_TO_SELF, toX,
+ Animation.RELATIVE_TO_SELF, 0f,
+ Animation.RELATIVE_TO_SELF, 0f
+ );
+ slide.setDuration(duration);
+ slide.setInterpolator(sFastOutExtraSlowInInterpolator);
+ animationSet.addAnimation(slide);
+ return animationSet;
+ }
+ }
+
+ /** @hide */
+ public static Animation getOpenEnter() {
+ if (sOpenEnter == null) {
+ sOpenEnter = new ActivityAnimFactory()
+ .fromX(1.0f)
+ .toX(0.0f)
+ .build();
+ }
+ return sOpenEnter;
+ }
+
+ /** @hide */
+ public static Animation getOpenExit() {
+ if (sOpenExit == null) {
+ sOpenExit = new ActivityAnimFactory()
+ .fromX(0.0f)
+ .toX(-DISTANCE)
+ .build();
+ }
+ return sOpenExit;
+ }
+
+ /** @hide */
+ public static Animation getCloseEnter() {
+ if (sCloseEnter == null) {
+ sCloseEnter = new ActivityAnimFactory()
+ .fromX(-DISTANCE)
+ .toX(0.0f)
+ .build();
+ }
+ return sCloseEnter;
+ }
+
+ /** @hide */
+ public static Animation getCloseExit() {
+ if (sCloseExit == null) {
+ sCloseExit = new ActivityAnimFactory()
+ .fromX(0.0f)
+ .toX(1.0f)
+ .build();
+ }
+ return sCloseExit;
+ }
+ }
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 2aa28894b3ba5..361c27287ccac 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -854,6 +854,10 @@
+
+
+
+
@@ -941,6 +945,10 @@
android:knownCerts="@array/config_setContactsDefaultAccountKnownSigners"
android:featureFlag="android.provider.new_default_account_api_enabled"/>
+
+
+
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 127ca3c989840..61283953dc577 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -32,6 +32,7 @@
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Point;
@@ -54,6 +55,7 @@
import android.window.DesktopModeFlags;
import android.window.WindowContainerTransaction;
+import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -220,7 +222,7 @@ static void updateRelayoutParams(
boolean shouldSetTaskVisibilityPositionAndCrop,
boolean isStatusBarVisible,
boolean isKeyguardVisibleAndOccluded,
- InsetsState displayInsetsState,
+ DisplayController displayController,
boolean hasGlobalFocus,
@NonNull Region globalExclusionRegion,
boolean shouldSetBackground,
@@ -246,6 +248,8 @@ static void updateRelayoutParams(
|| (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
relayoutParams.mDisplayExclusionRegion.set(globalExclusionRegion);
relayoutParams.mInSyncWithTransition = inSyncWithTransition;
+ relayoutParams.mCornerRadius =
+ getCornerRadius(context, displayController.getDisplay(taskInfo.displayId));
if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
// If the app is requesting to customize the caption bar, allow input to fall
@@ -265,7 +269,8 @@ static void updateRelayoutParams(
);
relayoutParams.mCaptionTopPadding = getTopPadding(relayoutParams,
- taskInfo.getConfiguration().windowConfiguration.getBounds(), displayInsetsState);
+ taskInfo.getConfiguration().windowConfiguration.getBounds(),
+ displayController.getInsetsState(taskInfo.displayId));
// Set opaque background for all freeform tasks to prevent freeform tasks below
// from being visible if freeform task window above is translucent.
// Otherwise if fluid resize is enabled, add a background to freeform tasks.
@@ -290,7 +295,7 @@ void relayout(RunningTaskInfo taskInfo,
updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible,
mIsKeyguardVisibleAndOccluded,
- mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
+ mDisplayController, hasGlobalFocus,
globalExclusionRegion, mDesktopConfig.shouldSetBackground(taskInfo),
inSyncWithTransition);
@@ -350,6 +355,19 @@ void relayout(RunningTaskInfo taskInfo,
});
}
+ private static int getCornerRadius(Context context, Display display) {
+ // Show rounded corners only on the internal display as we can't get rounded corners for
+ // external displays.
+ if (display.getType() != Display.TYPE_INTERNAL) {
+ return 0;
+ }
+ final TypedArray ta = context.obtainStyledAttributes(
+ new int[]{android.R.attr.dialogCornerRadius});
+ final int cornerRadius = ta.getDimensionPixelSize(0, 0);
+ ta.recycle();
+ return cornerRadius;
+ }
+
/**
* Sets up listeners when a new root view is created.
*/
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index eed14fe39d560..f2466723955d6 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -409,6 +409,9 @@
+
+
+
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 5eb23a08baa4f..bef7291633232 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -44,6 +44,7 @@
import com.android.systemui.qs.QSPanelController;
import com.android.systemui.shared.statusbar.phone.BarTransitions;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.policy.GameSpaceManager;
import com.android.systemui.util.Compile;
import java.io.PrintWriter;
@@ -350,4 +351,6 @@ void setIsLaunchingActivityOverLockscreen(
*/
ActivityTransitionAnimator.Controller getAnimatorControllerFromNotification(
ExpandableNotificationRow associatedView);
+
+ GameSpaceManager getGameSpaceManager();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 86e6c4a514385..dfe0e5a6b9120 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -209,6 +209,7 @@
import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
+import com.android.systemui.statusbar.policy.GameSpaceManager;
import com.android.systemui.statusbar.notification.init.NotificationsController;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -467,6 +468,8 @@ public void toggleCameraFlash() {
private final UserTracker mUserTracker;
private final ActivityStarter mActivityStarter;
+ private GameSpaceManager mGameSpaceManager;
+
private final DisplayMetrics mDisplayMetrics;
// XXX: gesture research
@@ -848,6 +851,8 @@ public CentralSurfacesImpl(
mActivityIntentHelper = new ActivityIntentHelper(mContext);
mActivityTransitionAnimator = activityTransitionAnimator;
+ mGameSpaceManager = new GameSpaceManager(mContext, mKeyguardStateController);
+
// TODO(b/190746471): Find a better home for this.
DateTimeView.setReceiverHandler(timeTickHandler);
@@ -1483,6 +1488,13 @@ protected void registerBroadcastReceiver() {
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_CAMERA_GESTURE);
mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter, null, UserHandle.ALL);
+
+ mGameSpaceManager.observe();
+ }
+
+ @Override
+ public GameSpaceManager getGameSpaceManager() {
+ return mGameSpaceManager;
}
protected QS createDefaultQSFragment() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 665681152cbdf..1c612a4fcd2fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -79,6 +79,7 @@
import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
+import com.android.systemui.statusbar.policy.GameSpaceManager;
import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController;
@@ -147,6 +148,7 @@ boolean onDismiss(PendingIntent intent, boolean isActivityIntent, boolean animat
private final MetricsLogger mMetricsLogger;
private final StatusBarNotificationActivityStarterLogger mLogger;
+ private final CentralSurfaces mCentralSurfaces;
private final NotificationPresenter mPresenter;
private final PanelExpansionInteractor mPanelExpansionInteractor;
private final NotificationShadeWindowController mNotificationShadeWindowController;
@@ -185,6 +187,7 @@ boolean onDismiss(PendingIntent intent, boolean isActivityIntent, boolean animat
MetricsLogger metricsLogger,
StatusBarNotificationActivityStarterLogger logger,
OnUserInteractionCallback onUserInteractionCallback,
+ CentralSurfaces centralSurfaces,
NotificationPresenter presenter,
PanelExpansionInteractor panelExpansionInteractor,
NotificationShadeWindowController notificationShadeWindowController,
@@ -221,6 +224,7 @@ boolean onDismiss(PendingIntent intent, boolean isActivityIntent, boolean animat
mShadeAnimationInteractor = shadeAnimationInteractor;
mMetricsLogger = metricsLogger;
mLogger = logger;
+ mCentralSurfaces = centralSurfaces;
mOnUserInteractionCallback = onUserInteractionCallback;
mPresenter = presenter;
mActivityTransitionAnimator = activityTransitionAnimator;
@@ -691,6 +695,11 @@ private void removeHunAfterClick(ExpandableNotificationRow row) {
@VisibleForTesting
void launchFullScreenIntent(NotificationEntry entry) {
+ GameSpaceManager gameSpace = mCentralSurfaces.getGameSpaceManager();
+ if (gameSpace != null && gameSpace.shouldSuppressFullScreenIntent()) {
+ return;
+ }
+
// Skip if device is in VR mode.
if (mPresenter.isDeviceInVrMode()) {
mLogger.logFullScreenIntentSuppressedByVR(entry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/GameSpaceManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/GameSpaceManager.kt
new file mode 100644
index 0000000000000..39e105d41b4aa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/GameSpaceManager.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2021 Chaldeaprjkt
+ * Copyright (C) 2022-2024 crDroid Android Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.app.ActivityTaskManager
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.os.PowerManager
+import android.os.RemoteException
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
+
+import java.util.Arrays
+import javax.inject.Inject
+
+@SysUISingleton
+class GameSpaceManager @Inject constructor(
+ private val context: Context,
+ private val keyguardStateController: KeyguardStateController,
+) {
+ private val handler by lazy { GameSpaceHandler(Looper.getMainLooper()) }
+ private val taskManager by lazy { ActivityTaskManager.getService() }
+
+ private var activeGame: String? = null
+ private var isRegistered = false
+
+ private val taskStackChangeListener = object : TaskStackChangeListener {
+ override fun onTaskStackChanged() {
+ handler.sendEmptyMessage(MSG_UPDATE_FOREGROUND_APP)
+ }
+ }
+
+ private val interactivityReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ when (intent?.action) {
+ Intent.ACTION_SCREEN_OFF -> {
+ activeGame = null
+ handler.sendEmptyMessage(MSG_DISPATCH_FOREGROUND_APP)
+ }
+ }
+ }
+ }
+
+ private val keyguardStateCallback = object : KeyguardStateController.Callback {
+ override fun onKeyguardShowingChanged() {
+ if (keyguardStateController.isShowing) return
+ handler.sendEmptyMessage(MSG_UPDATE_FOREGROUND_APP)
+ }
+ }
+
+ private inner class GameSpaceHandler(looper: Looper) : Handler(looper, null, true) {
+ override fun handleMessage(msg: Message) {
+ when (msg.what) {
+ MSG_UPDATE_FOREGROUND_APP -> checkForegroundApp()
+ MSG_DISPATCH_FOREGROUND_APP -> dispatchForegroundApp()
+ }
+ }
+ }
+
+ private fun checkForegroundApp() {
+ try {
+ val info = taskManager.focusedRootTaskInfo
+ info?.topActivity ?: return
+ val packageName = info.topActivity?.packageName
+ activeGame = checkGameList(packageName)
+ handler.sendEmptyMessage(MSG_DISPATCH_FOREGROUND_APP)
+ } catch (e: RemoteException) {
+ }
+ }
+
+ private fun dispatchForegroundApp() {
+ val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+ if (!pm.isInteractive && activeGame != null) return
+ val action = if (activeGame != null) ACTION_GAME_START else ACTION_GAME_STOP
+ Intent(action).apply {
+ setPackage(GAMESPACE_PACKAGE)
+ component = ComponentName.unflattenFromString(RECEIVER_CLASS)
+ putExtra(EXTRA_CALLER_NAME, context.packageName)
+ if (activeGame != null) putExtra(EXTRA_ACTIVE_GAME, activeGame)
+ addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+ or Intent.FLAG_RECEIVER_FOREGROUND
+ or Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND)
+ context.sendBroadcastAsUser(this, UserHandle.CURRENT,
+ android.Manifest.permission.MANAGE_GAME_MODE)
+ }
+ }
+
+ fun observe() {
+ val taskStackChangeListeners = TaskStackChangeListeners.getInstance();
+ if (isRegistered) {
+ taskStackChangeListeners.unregisterTaskStackListener(taskStackChangeListener)
+ }
+ taskStackChangeListeners.registerTaskStackListener(taskStackChangeListener)
+ isRegistered = true;
+ handler.sendEmptyMessage(MSG_UPDATE_FOREGROUND_APP)
+ context.registerReceiver(interactivityReceiver, IntentFilter().apply {
+ addAction(Intent.ACTION_SCREEN_OFF)
+ }, Context.RECEIVER_NOT_EXPORTED)
+ keyguardStateController.addCallback(keyguardStateCallback)
+ }
+
+ fun unobserve() {
+ val taskStackChangeListeners = TaskStackChangeListeners.getInstance();
+ if (!isRegistered) {
+ taskStackChangeListeners.unregisterTaskStackListener(taskStackChangeListener)
+ }
+ isRegistered = false;
+ context.unregisterReceiver(interactivityReceiver)
+ keyguardStateController.removeCallback(keyguardStateCallback)
+ }
+
+ fun isGameActive() = activeGame != null
+
+ fun shouldSuppressFullScreenIntent() =
+ Settings.System.getIntForUser(
+ context.contentResolver,
+ Settings.System.GAMESPACE_SUPPRESS_FULLSCREEN_INTENT, 0,
+ UserHandle.USER_CURRENT) == 1 && isGameActive()
+
+ private fun checkGameList(packageName: String?): String? {
+ packageName ?: return null
+ val games = Settings.System.getStringForUser(
+ context.contentResolver,
+ Settings.System.GAMESPACE_GAME_LIST,
+ UserHandle.USER_CURRENT)
+
+ if (games.isNullOrEmpty())
+ return null
+
+ return games.split(";")
+ .map { it.split("=").first() }
+ .firstOrNull { it == packageName }
+ }
+
+ companion object {
+ private const val ACTION_GAME_START = "io.chaldeaprjkt.gamespace.action.GAME_START"
+ private const val ACTION_GAME_STOP = "io.chaldeaprjkt.gamespace.action.GAME_STOP"
+ private const val GAMESPACE_PACKAGE = "io.chaldeaprjkt.gamespace"
+ private const val RECEIVER_CLASS = "io.chaldeaprjkt.gamespace/.gamebar.GameBroadcastReceiver"
+ private const val EXTRA_CALLER_NAME = "source"
+ private const val EXTRA_ACTIVE_GAME = "package_name"
+ private const val MSG_UPDATE_FOREGROUND_APP = 0
+ private const val MSG_DISPATCH_FOREGROUND_APP = 1
+ }
+}
diff --git a/services/Android.bp b/services/Android.bp
index 6f978252e57c4..d22024fd7e5e6 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -133,6 +133,7 @@ filegroup {
":services.coverage-sources",
":services.credentials-sources",
":services.devicepolicy-sources",
+ ":services.freeform-sources",
":services.midi-sources",
":services.musicsearch-sources",
":services.net-sources",
@@ -339,6 +340,7 @@ java_library {
"services.credentials",
"services.devicepolicy",
"services.flags",
+ "services.freeform",
"services.midi",
"services.musicsearch",
"services.net",
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 71091f230ad1f..77abaa02e1334 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -134,6 +134,7 @@ java_library_static {
":display-layout-config",
":display-topology",
":device-state-config",
+ ":lmofreeform-display-adapter-java",
"java/com/android/server/EventLogTags.logtags",
"java/com/android/server/am/EventLogTags.logtags",
"java/com/android/server/wm/EventLogTags.logtags",
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index fa19beb2c7d75..93ba3bbebefcf 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -48,6 +48,7 @@
import android.app.StatsManager;
import android.app.UidObserver;
import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -60,6 +61,7 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
+import android.database.ContentObserver;
import android.hardware.power.Mode;
import android.net.Uri;
import android.os.Binder;
@@ -81,6 +83,7 @@
import android.os.UserManager;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
+import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.AtomicFile;
@@ -1593,6 +1596,10 @@ public void onReceive(Context context, Intent intent) {
mGameDefaultFrameRateValue = (float) mSysProps.getInt(
PROPERTY_RO_SURFACEFLINGER_GAME_DEFAULT_FRAME_RATE, 60);
Slog.v(TAG, "Game Default Frame Rate : " + mGameDefaultFrameRateValue);
+
+ // Start to observe our Settings.Secure.GAME_OVERLAY
+ // after boot completed.
+ new SettingsObserver(mHandler);
}
private void sendUserMessage(int userId, int what, String eventForLog, int delayMillis) {
@@ -2364,4 +2371,41 @@ private void handleUidMovedOffTop(int uid) {
}
}
}
+
+ class SettingsObserver extends ContentObserver {
+
+ private final ContentResolver mContentResolver;
+
+ SettingsObserver(Handler handler) {
+ super(handler);
+ mContentResolver = mContext.getContentResolver();
+ mContentResolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.GAME_OVERLAY), false, this,
+ UserHandle.USER_ALL);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ String newValue = Settings.Secure.getString(mContentResolver,
+ Settings.Secure.GAME_OVERLAY);
+ // We write key and value of the device_config property as a single string
+ // from our GameSpace.
+ // ';;' is the separator betweeen key and value.
+ // Example: com.libremobileos.game;;mode=2,downscaleFactor=0.7:mode=3,downscaleFactor=0.8
+ // So split the key and value from the string
+ // and set the device_config propery.
+ String[] parsedValues = newValue.split(";;");
+ // Value should contain both package name and config.
+ // Otherwise don't do anything.
+ if (parsedValues.length < 2) return;
+ // We don't need to care about any format and all.
+ // It will be handled by the GamePackageConfiguration while
+ // parsing the device_config property.
+ String packageName = parsedValues[0];
+ String configValue = parsedValues[1];
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_GAME_OVERLAY,
+ packageName, configValue, false);
+ }
+ }
+
}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index e032b4e332007..f38425c326fe2 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -208,6 +208,8 @@
import com.android.server.wm.SurfaceAnimationThread;
import com.android.server.wm.WindowManagerInternal;
+import com.libremobileos.freeform.ILMOFreeformDisplayCallback;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -563,6 +565,8 @@ public synchronized void requestDisplayState(int displayId, int state, float bri
// only be used for the devices in projected mode.
private boolean mIncludeDefaultDisplayInTopology;
+ private LMOFreeformDisplayAdapter mFreeformDisplayAdapter;
+
private final BroadcastReceiver mIdleModeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -2386,6 +2390,7 @@ private void registerAdditionalDisplayAdapters() {
if (shouldRegisterNonEssentialDisplayAdaptersLocked()) {
registerOverlayDisplayAdapterLocked();
registerWifiDisplayAdapterLocked();
+ registerFreeformDisplayAdapterLocked();
}
}
}
@@ -2406,6 +2411,13 @@ private void registerWifiDisplayAdapterLocked() {
}
}
+ private void registerFreeformDisplayAdapterLocked() {
+ mFreeformDisplayAdapter = new LMOFreeformDisplayAdapter(
+ mSyncRoot, mContext, mHandler, mDisplayDeviceRepo, mLogicalDisplayMapper,
+ mUiHandler, mFlags);
+ registerDisplayAdapterLocked(mFreeformDisplayAdapter);
+ }
+
private boolean shouldRegisterNonEssentialDisplayAdaptersLocked() {
// In safe mode, we disable non-essential display adapters to give the user
// an opportunity to fix broken settings or other problems that might affect
@@ -6431,6 +6443,23 @@ public void reloadTopologies(final int userId) {
scheduleTopologiesReload(mCurrentUserId, /*isUserSwitching=*/ false);
}
}
+
+ public void createFreeformLocked(String name, ILMOFreeformDisplayCallback callback,
+ int width, int height, int densityDpi, boolean secure, boolean ownContentOnly,
+ boolean shouldShowSystemDecorations, Surface surface, float refreshRate,
+ long presentationDeadlineNanos) {
+ mFreeformDisplayAdapter.createFreeformLocked(name, callback, width, height, densityDpi,
+ secure, ownContentOnly, shouldShowSystemDecorations, surface, refreshRate,
+ presentationDeadlineNanos);
+ }
+
+ public void resizeFreeform(IBinder appToken, int width, int height, int densityDpi) {
+ mFreeformDisplayAdapter.resizeFreeform(appToken, width, height, densityDpi);
+ }
+
+ public void releaseFreeform(IBinder appToken) {
+ mFreeformDisplayAdapter.releaseFreeform(appToken);
+ }
}
class DesiredDisplayModeSpecsObserver
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index ac8b92854e68b..8c458ae6f6a42 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -3070,6 +3070,12 @@ int startActivityFromRecents(int callingPid, int callingUid, int taskId,
mService.continueWindowLayout();
}
}
+ if (activityOptions != null) {
+ final int windowingMode = activityOptions.getLaunchWindowingMode();
+ if (windowingMode == WINDOWING_MODE_FREEFORM) {
+ task.setBounds(activityOptions.getLaunchBounds());
+ }
+ }
taskCallingUid = task.mCallingUid;
callingPackage = task.mCallingPackage;
callingFeatureId = task.mCallingFeatureId;
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 27427724f0c3e..3b79e0650ee93 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -130,6 +130,7 @@
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_SLEEP_TOKEN;
import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_TRANSACTIONS;
import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN;
+import static com.android.server.display.LMOFreeformDisplayAdapter.UNIQUE_ID_PREFIX;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
@@ -563,6 +564,8 @@ public void onIdMatch(InsetsSource source1, InsetsSource source2) {
// TODO(multi-display): remove some of the usages.
boolean isDefaultDisplay;
+ private boolean isFreeformDisplay;
+
/** Save allocating when calculating rects */
private final Rect mTmpRect = new Rect();
private final Region mTmpRegion = new Region();
@@ -796,6 +799,8 @@ public void onIdMatch(InsetsSource source1, InsetsSource source2) {
/** Last window to hold the screen locked. */
private WindowState mLastWakeLockHoldingWindow;
+ private boolean mHasSecureContent;
+
/**
* Whether display is allowed to ignore all activity size restrictions.
* @see #isDisplayIgnoreActivitySizeRestrictions
@@ -1169,6 +1174,7 @@ public void onLowMemory() {
mSystemGestureExclusionLimit = mWmService.mConstants.mSystemGestureExclusionLimitDp
* mDisplayMetrics.densityDpi / DENSITY_DEFAULT;
isDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;
+ isFreeformDisplay = mDisplayInfo.uniqueId.startsWith(UNIQUE_ID_PREFIX);
mInsetsStateController = new InsetsStateController(this);
initializeDisplayBaseInfo();
mDisplayFrames = new DisplayFrames(mInsetsStateController.getRawInsetsState(),
@@ -2147,7 +2153,9 @@ void continueUpdateOrientationForDiffOrienLaunchingApp() {
return;
}
// The orientation of display is not changed.
- clearFixedRotationLaunchingApp();
+ if (!mTransitionController.isCollecting(this)) {
+ clearFixedRotationLaunchingApp();
+ }
}
/**
@@ -4362,6 +4370,11 @@ InsetsControlTarget getImeControlTarget() {
return mWmService.mDisplayWindowSettings.getImePolicyLocked(this);
}
+ boolean forceDesktopMode() {
+ return ("VNC".equals(mDisplay.getName()) || mWmService.mForceDesktopModeOnExternalDisplays)
+ && !isDefaultDisplay && !isFreeformDisplay && !isPrivate();
+ }
+
/** @see WindowManagerInternal#onToggleImeRequested */
void onShowImeRequested() {
if (mInputMethodWindow == null) {
@@ -5151,6 +5164,11 @@ boolean isInputMethodClientFocus(int uid, int pid) {
return imeLayeringTarget.mSession.mUid == uid && imeLayeringTarget.mSession.mPid == pid;
}
+ boolean hasSecureWindowOnScreen() {
+ final WindowState win = getWindow(w -> w.isOnScreen() && w.isSecureLocked());
+ return win != null;
+ }
+
// TODO: Super unexpected long method that should be broken down...
void applySurfaceChangesTransaction() {
final WindowSurfacePlacer surfacePlacer = mWmService.mWindowPlacerLocked;
@@ -5286,6 +5304,13 @@ private void performLayoutNoTrace(boolean initial, boolean updateInputWindows) {
if (updateInputWindows) {
mInputMonitor.updateInputWindowsLw(false /*force*/);
}
+
+ // Notify if display added or removed a secure window
+ final boolean hasSecureContent = hasSecureWindowOnScreen();
+ if (hasSecureContent != mHasSecureContent) {
+ mHasSecureContent = hasSecureContent;
+ mWmService.notifyDisplaySecureContentChange(mDisplayId, hasSecureContent);
+ }
}
/**
@@ -5780,7 +5805,7 @@ && isPublicSecondaryDisplayWithDesktopModeForceEnabled()) {
* also check {@link #isSystemDecorationsSupported()} to avoid breaking any security policy.
*/
boolean isPublicSecondaryDisplayWithDesktopModeForceEnabled() {
- if (!mWmService.mForceDesktopModeOnExternalDisplays || isDefaultDisplay || isPrivate()) {
+ if (!mWmService.mForceDesktopModeOnExternalDisplays || isDefaultDisplay || isPrivate() && !isFreeformDisplay) {
return false;
}
if (!isWindowingModeSupported(WINDOWING_MODE_FREEFORM)) {
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index bfbdef1a92cef..9d24cba7b9ba1 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -259,16 +259,13 @@ public int getPointerLayer() {
@Override
public int getPointerDisplayId() {
synchronized (mService.mGlobalLock) {
- // If desktop mode is not enabled, show on the default display.
- if (!mService.mForceDesktopModeOnExternalDisplays) {
- return DEFAULT_DISPLAY;
- }
-
// Look for the topmost freeform display.
int firstExternalDisplayId = DEFAULT_DISPLAY;
for (int i = mService.mRoot.mChildren.size() - 1; i >= 0; --i) {
final DisplayContent displayContent = mService.mRoot.mChildren.get(i);
- if (displayContent.getDisplayInfo().state == Display.STATE_OFF) {
+ if (displayContent.getDisplayInfo().state == Display.STATE_OFF
+ || !displayContent.forceDesktopMode()) {
+ // If desktop mode is not enabled, show on the default display.
continue;
}
// Heuristic solution here. Currently when "Freeform windows" developer option is
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 8931af7c0474c..4216ada8dd65c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -439,6 +439,14 @@ default void dragRecipientEntered(IWindow window) {}
default void dragRecipientExited(IWindow window) {}
}
+ /**
+ * Listener interface for secure content showing up on the display.
+ */
+ public interface DisplaySecureContentListener {
+ public void onDisplayHasSecureWindowOnScreenChanged(
+ int displayId, boolean hasSecureWindowOnScreen);
+ }
+
/**
* Request the interface to access features implemented by AccessibilityController.
*/
@@ -1249,4 +1257,12 @@ public abstract void requestAssistScreenshot(IAssistDataReceiver receiver,
* @throws RuntimeException if the payload cannot be written to the settings file.
*/
public abstract void restoreDisplayWindowSettings(int userId, byte[] payload);
+
+ /**
+ * Register/unregister callbacks for secure content showing up on the display.
+ */
+ public abstract void registerDisplaySecureContentListener(
+ DisplaySecureContentListener listener);
+ public abstract void unregisterDisplaySecureContentListener(
+ DisplaySecureContentListener listener);
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index dd4c992494b0a..20a67fa901604 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -797,6 +797,9 @@ public void binderDied() {
WindowManagerInternal.OnHardKeyboardStatusChangeListener mHardKeyboardStatusChangeListener;
WindowManagerInternal.OnImeRequestedChangedListener mOnImeRequestedChangedListener;
+ private ArraySet
+ mDisplaySecureContentListeners = new ArraySet<>();
+
SettingsObserver mSettingsObserver;
final EmbeddedWindowController mEmbeddedWindowController;
final AnrController mAnrController;
@@ -5817,6 +5820,15 @@ void notifyHardKeyboardStatusChange() {
}
}
+ void notifyDisplaySecureContentChange(int displayId, boolean hasSecureWindowOnScreen) {
+ synchronized (mGlobalLock) {
+ mDisplaySecureContentListeners.forEach((listener) -> {
+ listener.onDisplayHasSecureWindowOnScreenChanged(
+ displayId, hasSecureWindowOnScreen);
+ });
+ }
+ }
+
// -------------------------------------------------------------
// Input Events and Focus Management
// -------------------------------------------------------------
@@ -9214,6 +9226,20 @@ public void requestAssistScreenshot(IAssistDataReceiver receiver, IBinder activi
}
WindowManagerService.this.requestAssistScreenshotInternal(receiver, displayId);
}
+
+ @Override
+ public void registerDisplaySecureContentListener(DisplaySecureContentListener listener) {
+ synchronized (mGlobalLock) {
+ mDisplaySecureContentListeners.add(listener);
+ }
+ }
+
+ @Override
+ public void unregisterDisplaySecureContentListener(DisplaySecureContentListener listener) {
+ synchronized (mGlobalLock) {
+ mDisplaySecureContentListeners.remove(listener);
+ }
+ }
}
/** Called to inform window manager if non-Vr UI shoul be disabled or not. */
diff --git a/services/core/java/com/android/server/wm/WindowOrientationListener.java b/services/core/java/com/android/server/wm/WindowOrientationListener.java
index d26616edfb44c..0cdd058a09994 100644
--- a/services/core/java/com/android/server/wm/WindowOrientationListener.java
+++ b/services/core/java/com/android/server/wm/WindowOrientationListener.java
@@ -1177,6 +1177,10 @@ public void onSensorChanged(SensorEvent event) {
return;
}
}
+ if (mRotationResolverService == null) {
+ // Bail out because RotationResolverManagerService wasn't started
+ return;
+ }
String packageName = null;
if (mActivityTaskManagerInternal != null) {
diff --git a/services/freeform/Android.bp b/services/freeform/Android.bp
new file mode 100644
index 0000000000000..3920451176ca5
--- /dev/null
+++ b/services/freeform/Android.bp
@@ -0,0 +1,27 @@
+//
+// SPDX-FileCopyrightText: 2023 The LibreMobileOS Foundation
+// SPDX-FileCopyrightText: 2024-2025 crDroid Android Project
+// SPDX-License-Identifier: Apache-2.0
+//
+
+filegroup {
+ name: "services.freeform-sources",
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.kt"
+ ],
+ path: "java",
+ visibility: ["//frameworks/base/services"],
+}
+
+java_library_static {
+ name: "services.freeform",
+ defaults: ["platform_service_defaults"],
+ srcs: [":services.freeform-sources"],
+ libs: ["services.core"],
+ static_libs: [
+ "kotlinx_coroutines",
+ "lmofreeform-server",
+ ],
+ jarjar_rules: "jarjar-rules.txt",
+}
diff --git a/services/freeform/jarjar-rules.txt b/services/freeform/jarjar-rules.txt
new file mode 100644
index 0000000000000..a50af24f3dddc
--- /dev/null
+++ b/services/freeform/jarjar-rules.txt
@@ -0,0 +1,2 @@
+rule kotlin.** com.libremobileos.server.jarjar.@0
+rule kotlinx.** com.libremobileos.server.jarjar.@0
diff --git a/services/freeform/java/com/android/server/display/FreeformService.java b/services/freeform/java/com/android/server/display/FreeformService.java
new file mode 100644
index 0000000000000..af5c3a5484ecc
--- /dev/null
+++ b/services/freeform/java/com/android/server/display/FreeformService.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023-2024 LibreMobileOS Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import android.content.Context;
+import android.hardware.display.DisplayManagerInternal;
+import android.util.Slog;
+
+import com.android.server.SystemService;
+
+import com.libremobileos.freeform.server.LMOFreeformService;
+import com.libremobileos.freeform.server.LMOFreeformServiceHolder;
+import com.libremobileos.freeform.server.LMOFreeformUIService;
+
+public class FreeformService extends SystemService {
+
+ private static final String TAG = "FreeformService";
+
+ public FreeformService(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ // noop
+ }
+
+ @Override
+ public void onBootPhase(@BootPhase int phase) {
+ if (phase != PHASE_ACTIVITY_MANAGER_READY || isSafeMode()) return;
+
+ Slog.d(TAG, "PHASE_ACTIVITY_MANAGER_READY, going to init!");
+
+ DisplayManagerInternal displayManager = getLocalService(DisplayManagerInternal.class);
+ if (displayManager == null) {
+ Slog.e(TAG, "Cannot init: DisplayManagerInternal is null!");
+ return;
+ }
+
+ LMOFreeformService service = new LMOFreeformService(displayManager);
+ LMOFreeformUIService uiService =
+ new LMOFreeformUIService(getContext(), displayManager, service);
+ LMOFreeformServiceHolder.init(uiService, service);
+ }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c1bfbb330b96f..cb69348f6cbed 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -175,6 +175,7 @@
import com.android.server.devicepolicy.DevicePolicyManagerService;
import com.android.server.devicestate.DeviceStateManagerService;
import com.android.server.display.DisplayManagerService;
+import com.android.server.display.FreeformService;
import com.android.server.display.color.ColorDisplayService;
import com.android.server.dreams.DreamManagerService;
import com.android.server.emergency.EmergencyAffordanceService;
@@ -2817,6 +2818,10 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
t.traceEnd();
}
+ t.traceBegin("FreeformService");
+ mSystemServiceManager.startService(FreeformService.class);
+ t.traceEnd();
+
if (!isWatch) {
// We don't run this on watches as there are no plans to use the data logged
// on watch devices.