From 10d7cb3c0f587eba5608f6e32f4f1339ef6680fd Mon Sep 17 00:00:00 2001 From: shawnlin Date: Thu, 17 Feb 2022 20:12:37 +0800 Subject: [PATCH] Exclude nav bar area from apps bounds when in kids mode GKS requested that app's bounds should not overlap with the nav bar area when kids mode is enabled except for Launcher. Creates a dedicate task organizer for kids mode. When kids mode is enabled, create a root task with its bounds excluding the nav bar area and tasks except launcher will be launched into it. Bug: 161689887 Test: manual: 1. adb shell settings put secure "nav_bar_force_visible" "1" 2. choose 3-buttons navigation mode in Settings>System>Gestures>System navigation 3. check winscope and see if bounds are set correctly Test: atest KidsModeTaskOrganizerTest Test: atest WindowOrganizerTests Change-Id: I93af1bc5b2696654d8ca5055dbbbebf5a41f89aa --- .../wm/shell/ShellCommandHandlerImpl.java | 7 + .../com/android/wm/shell/ShellInitImpl.java | 7 + .../android/wm/shell/ShellTaskOrganizer.java | 4 +- .../wm/shell/common/DisplayController.java | 8 + .../wm/shell/dagger/WMShellBaseModule.java | 24 +- .../shell/kidsmode/KidsModeTaskOrganizer.java | 341 ++++++++++++++++++ .../kidsmode/KidsModeTaskOrganizerTest.java | 149 ++++++++ .../core/java/com/android/server/wm/Task.java | 6 +- 8 files changed, 540 insertions(+), 6 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java index 908a31dc3e4e8..8483f070d06ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java @@ -21,6 +21,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import com.android.wm.shell.apppairs.AppPairsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; +import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; @@ -46,11 +47,13 @@ public final class ShellCommandHandlerImpl { private final Optional mAppPairsOptional; private final Optional mRecentTasks; private final ShellTaskOrganizer mShellTaskOrganizer; + private final KidsModeTaskOrganizer mKidsModeTaskOrganizer; private final ShellExecutor mMainExecutor; private final HandlerImpl mImpl = new HandlerImpl(); public ShellCommandHandlerImpl( ShellTaskOrganizer shellTaskOrganizer, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional legacySplitScreenOptional, Optional splitScreenOptional, Optional pipOptional, @@ -60,6 +63,7 @@ public final class ShellCommandHandlerImpl { Optional recentTasks, ShellExecutor mainExecutor) { mShellTaskOrganizer = shellTaskOrganizer; + mKidsModeTaskOrganizer = kidsModeTaskOrganizer; mRecentTasks = recentTasks; mLegacySplitScreenOptional = legacySplitScreenOptional; mSplitScreenOptional = splitScreenOptional; @@ -92,6 +96,9 @@ public final class ShellCommandHandlerImpl { pw.println(); pw.println(); mRecentTasks.ifPresent(recentTasks -> recentTasks.dump(pw, "")); + pw.println(); + pw.println(); + mKidsModeTaskOrganizer.dump(pw, ""); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java index c3ce3627fb0bd..b729fe1e55dc6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java @@ -29,6 +29,7 @@ import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.freeform.FreeformTaskListener; import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.fullscreen.FullscreenUnfoldController; +import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -48,6 +49,7 @@ public class ShellInitImpl { private final DisplayInsetsController mDisplayInsetsController; private final DragAndDropController mDragAndDropController; private final ShellTaskOrganizer mShellTaskOrganizer; + private final KidsModeTaskOrganizer mKidsModeTaskOrganizer; private final Optional mBubblesOptional; private final Optional mSplitScreenOptional; private final Optional mAppPairsOptional; @@ -68,6 +70,7 @@ public class ShellInitImpl { DisplayInsetsController displayInsetsController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional bubblesOptional, Optional splitScreenOptional, Optional appPairsOptional, @@ -84,6 +87,7 @@ public class ShellInitImpl { mDisplayInsetsController = displayInsetsController; mDragAndDropController = dragAndDropController; mShellTaskOrganizer = shellTaskOrganizer; + mKidsModeTaskOrganizer = kidsModeTaskOrganizer; mBubblesOptional = bubblesOptional; mSplitScreenOptional = splitScreenOptional; mAppPairsOptional = appPairsOptional; @@ -136,6 +140,9 @@ public class ShellInitImpl { mFullscreenUnfoldController.ifPresent(FullscreenUnfoldController::init); mRecentTasks.ifPresent(RecentTasksController::init); + + // Initialize kids mode task organizer + mKidsModeTaskOrganizer.initialize(mStartingWindow); } @ExternalThread diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 3f8343a7abd39..84429948c6432 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -196,8 +196,8 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } @VisibleForTesting - ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, ShellExecutor mainExecutor, - Context context, @Nullable CompatUIController compatUI, + protected ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, + ShellExecutor mainExecutor, Context context, @Nullable CompatUIController compatUI, Optional recentTasks) { super(taskOrganizerController, mainExecutor); mCompatUI = compatUI; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index d22fb50390527..4ba32e93fb3de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -109,6 +109,14 @@ public class DisplayController { return r != null ? r.mContext : null; } + /** + * Get the InsetsState of a display. + */ + public InsetsState getInsetsState(int displayId) { + final DisplayRecord r = mDisplays.get(displayId); + return r != null ? r.mInsetsState : null; + } + /** * Updates the insets for a given display. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 0362b3fba36b8..bf0337dbd11a8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -68,6 +68,7 @@ import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.fullscreen.FullscreenUnfoldController; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; +import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.onehanded.OneHanded; @@ -184,7 +185,23 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Optional provideCompatUI(CompatUIController compatUIController) { + static KidsModeTaskOrganizer provideKidsModeTaskOrganizer( + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler, + Context context, + CompatUIController compatUI, + SyncTransactionQueue syncTransactionQueue, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + Optional recentTasksOptional + ) { + return new KidsModeTaskOrganizer(mainExecutor, mainHandler, context, compatUI, + syncTransactionQueue, displayController, displayInsetsController, + recentTasksOptional); + } + + @WMSingleton + @Provides static Optional provideCompatUI(CompatUIController compatUIController) { return Optional.of(compatUIController.asCompatUI()); } @@ -637,6 +654,7 @@ public abstract class WMShellBaseModule { DisplayInsetsController displayInsetsController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional bubblesOptional, Optional splitScreenOptional, Optional appPairsOptional, @@ -653,6 +671,7 @@ public abstract class WMShellBaseModule { displayInsetsController, dragAndDropController, shellTaskOrganizer, + kidsModeTaskOrganizer, bubblesOptional, splitScreenOptional, appPairsOptional, @@ -680,6 +699,7 @@ public abstract class WMShellBaseModule { @Provides static ShellCommandHandlerImpl provideShellCommandHandlerImpl( ShellTaskOrganizer shellTaskOrganizer, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional legacySplitScreenOptional, Optional splitScreenOptional, Optional pipOptional, @@ -688,7 +708,7 @@ public abstract class WMShellBaseModule { Optional appPairsOptional, Optional recentTasksOptional, @ShellMainThread ShellExecutor mainExecutor) { - return new ShellCommandHandlerImpl(shellTaskOrganizer, + return new ShellCommandHandlerImpl(shellTaskOrganizer, kidsModeTaskOrganizer, legacySplitScreenOptional, splitScreenOptional, pipOptional, oneHandedOptional, hideDisplayCutout, appPairsOptional, recentTasksOptional, mainExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java new file mode 100644 index 0000000000000..429eb9963794b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2022 The Android Open Source 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.wm.shell.kidsmode; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.window.ITaskOrganizerController; +import android.window.TaskAppearedInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.policy.ForceShowNavigationBarSettingsObserver; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIController; +import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.startingsurface.StartingWindowController; + +import java.io.PrintWriter; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * A dedicated task organizer when kids mode is enabled. + * - Creates a root task with bounds that exclude the navigation bar area + * - Launch all task into the root task except for Launcher + */ +public class KidsModeTaskOrganizer extends ShellTaskOrganizer { + private static final String TAG = "KidsModeTaskOrganizer"; + + private static final int[] CONTROLLED_ACTIVITY_TYPES = + {ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD}; + private static final int[] CONTROLLED_WINDOWING_MODES = + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; + + private final Handler mMainHandler; + private final Context mContext; + private final SyncTransactionQueue mSyncQueue; + private final DisplayController mDisplayController; + private final DisplayInsetsController mDisplayInsetsController; + + @VisibleForTesting + ActivityManager.RunningTaskInfo mLaunchRootTask; + @VisibleForTesting + SurfaceControl mLaunchRootLeash; + @VisibleForTesting + final IBinder mCookie = new Binder(); + + private final InsetsState mInsetsState = new InsetsState(); + private int mDisplayWidth; + private int mDisplayHeight; + + private ForceShowNavigationBarSettingsObserver mForceShowNavigationBarSettingsObserver; + private boolean mEnabled; + + DisplayController.OnDisplaysChangedListener mOnDisplaysChangedListener = + new DisplayController.OnDisplaysChangedListener() { + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + final DisplayLayout displayLayout = + mDisplayController.getDisplayLayout(DEFAULT_DISPLAY); + if (displayLayout == null) { + return; + } + final int displayWidth = displayLayout.width(); + final int displayHeight = displayLayout.height(); + if (displayWidth == mDisplayWidth || displayHeight == mDisplayHeight) { + return; + } + mDisplayWidth = displayWidth; + mDisplayHeight = displayHeight; + updateBounds(); + } + }; + + DisplayInsetsController.OnInsetsChangedListener mOnInsetsChangedListener = + new DisplayInsetsController.OnInsetsChangedListener() { + @Override + public void insetsChanged(InsetsState insetsState) { + // Update bounds only when the insets of navigation bar or task bar is changed. + if (Objects.equals(insetsState.peekSource(InsetsState.ITYPE_NAVIGATION_BAR), + mInsetsState.peekSource(InsetsState.ITYPE_NAVIGATION_BAR)) + && Objects.equals(insetsState.peekSource( + InsetsState.ITYPE_EXTRA_NAVIGATION_BAR), + mInsetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR))) { + return; + } + mInsetsState.set(insetsState); + updateBounds(); + } + }; + + @VisibleForTesting + KidsModeTaskOrganizer( + ITaskOrganizerController taskOrganizerController, + ShellExecutor mainExecutor, + Handler mainHandler, + Context context, + @Nullable CompatUIController compatUI, + SyncTransactionQueue syncTransactionQueue, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + Optional recentTasks, + ForceShowNavigationBarSettingsObserver forceShowNavigationBarSettingsObserver) { + super(taskOrganizerController, mainExecutor, context, compatUI, recentTasks); + mContext = context; + mMainHandler = mainHandler; + mSyncQueue = syncTransactionQueue; + mDisplayController = displayController; + mDisplayInsetsController = displayInsetsController; + mForceShowNavigationBarSettingsObserver = forceShowNavigationBarSettingsObserver; + } + + public KidsModeTaskOrganizer( + ShellExecutor mainExecutor, + Handler mainHandler, + Context context, + @Nullable CompatUIController compatUI, + SyncTransactionQueue syncTransactionQueue, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + Optional recentTasks) { + super(mainExecutor, context, compatUI, recentTasks); + mContext = context; + mMainHandler = mainHandler; + mSyncQueue = syncTransactionQueue; + mDisplayController = displayController; + mDisplayInsetsController = displayInsetsController; + } + + /** + * Initializes kids mode status. + */ + public void initialize(StartingWindowController startingWindowController) { + initStartingWindow(startingWindowController); + if (mForceShowNavigationBarSettingsObserver == null) { + mForceShowNavigationBarSettingsObserver = new ForceShowNavigationBarSettingsObserver( + mMainHandler, mContext); + } + mForceShowNavigationBarSettingsObserver.setOnChangeRunnable(() -> updateKidsModeState()); + updateKidsModeState(); + mForceShowNavigationBarSettingsObserver.register(); + } + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mEnabled && mLaunchRootTask == null && taskInfo.launchCookies != null + && taskInfo.launchCookies.contains(mCookie)) { + mLaunchRootTask = taskInfo; + mLaunchRootLeash = leash; + updateTask(); + } + super.onTaskAppeared(taskInfo, leash); + + mSyncQueue.runInSync(t -> { + // Reset several properties back to fullscreen (PiP, for example, leaves all these + // properties in a bad state). + t.setCrop(leash, null); + t.setPosition(leash, 0, 0); + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + }); + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (mLaunchRootTask != null && mLaunchRootTask.taskId == taskInfo.taskId + && !taskInfo.equals(mLaunchRootTask)) { + mLaunchRootTask = taskInfo; + } + + super.onTaskInfoChanged(taskInfo); + } + + @VisibleForTesting + void updateKidsModeState() { + final boolean enabled = mForceShowNavigationBarSettingsObserver.isEnabled(); + if (mEnabled == enabled) { + return; + } + mEnabled = enabled; + if (mEnabled) { + enable(); + } else { + disable(); + } + } + + @VisibleForTesting + void enable() { + final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(DEFAULT_DISPLAY); + if (displayLayout != null) { + mDisplayWidth = displayLayout.width(); + mDisplayHeight = displayLayout.height(); + } + mInsetsState.set(mDisplayController.getInsetsState(DEFAULT_DISPLAY)); + mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, + mOnInsetsChangedListener); + mDisplayController.addDisplayWindowListener(mOnDisplaysChangedListener); + List taskAppearedInfos = registerOrganizer(); + for (int i = 0; i < taskAppearedInfos.size(); i++) { + final TaskAppearedInfo info = taskAppearedInfos.get(i); + onTaskAppeared(info.getTaskInfo(), info.getLeash()); + } + createRootTask(DEFAULT_DISPLAY, WINDOWING_MODE_FULLSCREEN, mCookie); + updateTask(); + } + + @VisibleForTesting + void disable() { + mDisplayInsetsController.removeInsetsChangedListener(DEFAULT_DISPLAY, + mOnInsetsChangedListener); + mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener); + updateTask(); + final WindowContainerToken token = mLaunchRootTask.token; + if (token != null) { + deleteRootTask(token); + } + mLaunchRootTask = null; + mLaunchRootLeash = null; + unregisterOrganizer(); + } + + private void updateTask() { + updateTask(getWindowContainerTransaction()); + } + + private void updateTask(WindowContainerTransaction wct) { + if (mLaunchRootTask == null || mLaunchRootLeash == null) { + return; + } + final Rect taskBounds = calculateBounds(); + final WindowContainerToken rootToken = mLaunchRootTask.token; + wct.setBounds(rootToken, mEnabled ? taskBounds : null); + wct.setLaunchRoot(rootToken, + mEnabled ? CONTROLLED_WINDOWING_MODES : null, + mEnabled ? CONTROLLED_ACTIVITY_TYPES : null); + wct.reparentTasks( + mEnabled ? null : rootToken /* currentParent */, + mEnabled ? rootToken : null /* newParent */, + CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES, + true /* onTop */); + wct.reorder(rootToken, mEnabled /* onTop */); + mSyncQueue.queue(wct); + final SurfaceControl rootLeash = mLaunchRootLeash; + mSyncQueue.runInSync(t -> { + t.setPosition(rootLeash, taskBounds.left, taskBounds.top); + t.setWindowCrop(rootLeash, taskBounds.width(), taskBounds.height()); + }); + } + + private Rect calculateBounds() { + final Rect bounds = new Rect(0, 0, mDisplayWidth, mDisplayHeight); + final InsetsSource navBarSource = mInsetsState.peekSource(InsetsState.ITYPE_NAVIGATION_BAR); + final InsetsSource taskBarSource = mInsetsState.peekSource( + InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + if (navBarSource != null && !navBarSource.getFrame().isEmpty()) { + bounds.inset(navBarSource.calculateInsets(bounds, false /* ignoreVisibility */)); + } else if (taskBarSource != null && !taskBarSource.getFrame().isEmpty()) { + bounds.inset(taskBarSource.calculateInsets(bounds, false /* ignoreVisibility */)); + } else { + bounds.setEmpty(); + } + return bounds; + } + + private void updateBounds() { + if (mLaunchRootTask == null) { + return; + } + final WindowContainerTransaction wct = getWindowContainerTransaction(); + final Rect taskBounds = calculateBounds(); + wct.setBounds(mLaunchRootTask.token, taskBounds); + mSyncQueue.queue(wct); + final SurfaceControl finalLeash = mLaunchRootLeash; + mSyncQueue.runInSync(t -> { + t.setPosition(finalLeash, taskBounds.left, taskBounds.top); + t.setWindowCrop(finalLeash, taskBounds.width(), taskBounds.height()); + }); + } + + @VisibleForTesting + WindowContainerTransaction getWindowContainerTransaction() { + return new WindowContainerTransaction(); + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + " mEnabled=" + mEnabled); + pw.println(innerPrefix + " mLaunchRootTask=" + mLaunchRootTask); + pw.println(innerPrefix + " mLaunchRootLeash=" + mLaunchRootLeash); + pw.println(innerPrefix + " mDisplayWidth=" + mDisplayWidth); + pw.println(innerPrefix + " mDisplayHeight=" + mDisplayHeight); + pw.println(innerPrefix + " mInsetsState=" + mInsetsState); + super.dump(pw, innerPrefix); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java new file mode 100644 index 0000000000000..78903dca7657c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2022 The Android Open Source 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.wm.shell.kidsmode; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.window.ITaskOrganizerController; +import android.window.TaskAppearedInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.policy.ForceShowNavigationBarSettingsObserver; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIController; +import com.android.wm.shell.startingsurface.StartingWindowController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Optional; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class KidsModeTaskOrganizerTest { + @Mock private ITaskOrganizerController mTaskOrganizerController; + @Mock private Context mContext; + @Mock private Handler mHandler; + @Mock private CompatUIController mCompatUI; + @Mock private SyncTransactionQueue mSyncTransactionQueue; + @Mock private ShellExecutor mTestExecutor; + @Mock private DisplayController mDisplayController; + @Mock private SurfaceControl mLeash; + @Mock private WindowContainerToken mToken; + @Mock private WindowContainerTransaction mTransaction; + @Mock private ForceShowNavigationBarSettingsObserver mObserver; + @Mock private StartingWindowController mStartingWindowController; + @Mock private DisplayInsetsController mDisplayInsetsController; + + KidsModeTaskOrganizer mOrganizer; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + try { + doReturn(ParceledListSlice.emptyList()) + .when(mTaskOrganizerController).registerTaskOrganizer(any()); + } catch (RemoteException e) { } + mOrganizer = spy(new KidsModeTaskOrganizer(mTaskOrganizerController, mTestExecutor, + mHandler, mContext, mCompatUI, mSyncTransactionQueue, mDisplayController, + mDisplayInsetsController, Optional.empty(), mObserver)); + mOrganizer.initialize(mStartingWindowController); + doReturn(mTransaction).when(mOrganizer).getWindowContainerTransaction(); + doReturn(new InsetsState()).when(mDisplayController).getInsetsState(DEFAULT_DISPLAY); + } + + @Test + public void testKidsModeOn() { + doReturn(true).when(mObserver).isEnabled(); + + mOrganizer.updateKidsModeState(); + + verify(mOrganizer, times(1)).enable(); + verify(mOrganizer, times(1)).registerOrganizer(); + verify(mOrganizer, times(1)).createRootTask( + eq(DEFAULT_DISPLAY), eq(WINDOWING_MODE_FULLSCREEN), eq(mOrganizer.mCookie)); + + final ActivityManager.RunningTaskInfo rootTask = createTaskInfo(12, + WINDOWING_MODE_FULLSCREEN, mOrganizer.mCookie); + mOrganizer.onTaskAppeared(rootTask, mLeash); + + assertThat(mOrganizer.mLaunchRootLeash).isEqualTo(mLeash); + assertThat(mOrganizer.mLaunchRootTask).isEqualTo(rootTask); + } + + @Test + public void testKidsModeOff() { + doReturn(true).when(mObserver).isEnabled(); + mOrganizer.updateKidsModeState(); + final ActivityManager.RunningTaskInfo rootTask = createTaskInfo(12, + WINDOWING_MODE_FULLSCREEN, mOrganizer.mCookie); + mOrganizer.onTaskAppeared(rootTask, mLeash); + + doReturn(false).when(mObserver).isEnabled(); + mOrganizer.updateKidsModeState(); + + + verify(mOrganizer, times(1)).disable(); + verify(mOrganizer, times(1)).unregisterOrganizer(); + verify(mOrganizer, times(1)).deleteRootTask(rootTask.token); + assertThat(mOrganizer.mLaunchRootLeash).isNull(); + assertThat(mOrganizer.mLaunchRootTask).isNull(); + } + + private ActivityManager.RunningTaskInfo createTaskInfo( + int taskId, int windowingMode, IBinder cookies) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.token = mToken; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + final ArrayList launchCookies = new ArrayList<>(); + if (cookies != null) { + launchCookies.add(cookies); + } + taskInfo.launchCookies = launchCookies; + return taskInfo; + } +} diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 7fe34f47c53ce..fed4e13cf7e3e 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -2043,8 +2043,10 @@ class Task extends TaskFragment { Rect outOverrideBounds = getResolvedOverrideConfiguration().windowConfiguration.getBounds(); if (windowingMode == WINDOWING_MODE_FULLSCREEN) { - // Use empty bounds to indicate "fill parent". - outOverrideBounds.setEmpty(); + if (!mCreatedByOrganizer) { + // Use empty bounds to indicate "fill parent". + outOverrideBounds.setEmpty(); + } // The bounds for fullscreen mode shouldn't be adjusted by minimal size. Otherwise if // the parent or display is smaller than the size, the content may be cropped. return;