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;