| /* |
| * 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_HOME; |
| 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.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; |
| import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; |
| import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| |
| import android.app.ActivityManager; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.res.Configuration; |
| import android.graphics.Rect; |
| import android.os.Binder; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.view.Display; |
| import android.view.InsetsSource; |
| import android.view.InsetsState; |
| import android.view.SurfaceControl; |
| import android.view.WindowInsets; |
| import android.window.ITaskOrganizerController; |
| import android.window.TaskAppearedInfo; |
| import android.window.WindowContainerToken; |
| import android.window.WindowContainerTransaction; |
| |
| import androidx.annotation.NonNull; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.VisibleForTesting; |
| 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.recents.RecentTasksController; |
| import com.android.wm.shell.sysui.ShellCommandHandler; |
| import com.android.wm.shell.sysui.ShellInit; |
| import com.android.wm.shell.unfold.UnfoldAnimationController; |
| |
| import java.io.PrintWriter; |
| import java.util.List; |
| 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, ACTIVITY_TYPE_HOME}; |
| private static final int[] CONTROLLED_WINDOWING_MODES = |
| {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; |
| |
| private final Handler mMainHandler; |
| private final Context mContext; |
| private final ShellCommandHandler mShellCommandHandler; |
| private final SyncTransactionQueue mSyncQueue; |
| private final DisplayController mDisplayController; |
| private final DisplayInsetsController mDisplayInsetsController; |
| |
| /** |
| * The value of the {@link R.bool.config_reverseDefaultRotation} property which defines how |
| * {@link Display#getRotation} values are mapped to screen orientations |
| */ |
| private final boolean mReverseDefaultRotationEnabled; |
| |
| @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 KidsModeSettingsObserver mKidsModeSettingsObserver; |
| private boolean mEnabled; |
| |
| private ActivityManager.RunningTaskInfo mHomeTask; |
| |
| private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| updateKidsModeState(); |
| } |
| }; |
| |
| 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) { |
| final boolean[] navigationBarChanged = {false}; |
| InsetsState.traverse(insetsState, mInsetsState, new InsetsState.OnTraverseCallbacks() { |
| @Override |
| public void onIdMatch(InsetsSource source1, InsetsSource source2) { |
| if (source1.getType() == WindowInsets.Type.navigationBars() |
| && !source1.equals(source2)) { |
| navigationBarChanged[0] = true; |
| } |
| } |
| |
| @Override |
| public void onIdNotFoundInState1(int index2, InsetsSource source2) { |
| if (source2.getType() == WindowInsets.Type.navigationBars()) { |
| navigationBarChanged[0] = true; |
| } |
| } |
| |
| @Override |
| public void onIdNotFoundInState2(int index1, InsetsSource source1) { |
| if (source1.getType() == WindowInsets.Type.navigationBars()) { |
| navigationBarChanged[0] = true; |
| } |
| } |
| }); |
| if (!navigationBarChanged[0]) { |
| return; |
| } |
| // Update bounds only when the insets of navigation bar or task bar is changed. |
| mInsetsState.set(insetsState); |
| updateBounds(); |
| } |
| }; |
| |
| @VisibleForTesting |
| KidsModeTaskOrganizer( |
| Context context, |
| ShellInit shellInit, |
| ShellCommandHandler shellCommandHandler, |
| ITaskOrganizerController taskOrganizerController, |
| SyncTransactionQueue syncTransactionQueue, |
| DisplayController displayController, |
| DisplayInsetsController displayInsetsController, |
| Optional<UnfoldAnimationController> unfoldAnimationController, |
| Optional<RecentTasksController> recentTasks, |
| KidsModeSettingsObserver kidsModeSettingsObserver, |
| ShellExecutor mainExecutor, |
| Handler mainHandler) { |
| // Note: we don't call super with the shell init because we will be initializing manually |
| super(/* shellInit= */ null, /* shellCommandHandler= */ null, taskOrganizerController, |
| /* compatUI= */ null, unfoldAnimationController, recentTasks, mainExecutor); |
| mContext = context; |
| mShellCommandHandler = shellCommandHandler; |
| mMainHandler = mainHandler; |
| mSyncQueue = syncTransactionQueue; |
| mDisplayController = displayController; |
| mDisplayInsetsController = displayInsetsController; |
| mKidsModeSettingsObserver = kidsModeSettingsObserver; |
| shellInit.addInitCallback(this::onInit, this); |
| mReverseDefaultRotationEnabled = context.getResources().getBoolean( |
| R.bool.config_reverseDefaultRotation); |
| } |
| |
| public KidsModeTaskOrganizer( |
| Context context, |
| ShellInit shellInit, |
| ShellCommandHandler shellCommandHandler, |
| SyncTransactionQueue syncTransactionQueue, |
| DisplayController displayController, |
| DisplayInsetsController displayInsetsController, |
| Optional<UnfoldAnimationController> unfoldAnimationController, |
| Optional<RecentTasksController> recentTasks, |
| ShellExecutor mainExecutor, |
| Handler mainHandler) { |
| // Note: we don't call super with the shell init because we will be initializing manually |
| super(/* shellInit= */ null, /* taskOrganizerController= */ null, /* compatUI= */ null, |
| unfoldAnimationController, recentTasks, mainExecutor); |
| mContext = context; |
| mShellCommandHandler = shellCommandHandler; |
| mMainHandler = mainHandler; |
| mSyncQueue = syncTransactionQueue; |
| mDisplayController = displayController; |
| mDisplayInsetsController = displayInsetsController; |
| shellInit.addInitCallback(this::onInit, this); |
| mReverseDefaultRotationEnabled = context.getResources().getBoolean( |
| R.bool.config_reverseDefaultRotation); |
| } |
| |
| /** |
| * Initializes kids mode status. |
| */ |
| public void onInit() { |
| if (mShellCommandHandler != null) { |
| mShellCommandHandler.addDumpCallback(this::dump, this); |
| } |
| if (mKidsModeSettingsObserver == null) { |
| mKidsModeSettingsObserver = new KidsModeSettingsObserver(mMainHandler, mContext); |
| } |
| mKidsModeSettingsObserver.setOnChangeRunnable(() -> updateKidsModeState()); |
| updateKidsModeState(); |
| mKidsModeSettingsObserver.register(); |
| |
| final IntentFilter filter = new IntentFilter(); |
| filter.addAction(Intent.ACTION_USER_SWITCHED); |
| mContext.registerReceiverForAllUsers(mUserSwitchIntentReceiver, filter, null, mMainHandler); |
| } |
| |
| @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); |
| |
| // Only allow home to draw under system bars. |
| if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) { |
| final WindowContainerTransaction wct = getWindowContainerTransaction(); |
| wct.setBounds(taskInfo.token, new Rect(0, 0, mDisplayWidth, mDisplayHeight)); |
| mSyncQueue.queue(wct); |
| mHomeTask = taskInfo; |
| } |
| 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; |
| } |
| |
| if (mHomeTask != null && mHomeTask.taskId == taskInfo.taskId |
| && !taskInfo.equals(mHomeTask)) { |
| mHomeTask = taskInfo; |
| } |
| |
| super.onTaskInfoChanged(taskInfo); |
| } |
| |
| @VisibleForTesting |
| void updateKidsModeState() { |
| final boolean enabled = mKidsModeSettingsObserver.isEnabled(); |
| if (mEnabled == enabled) { |
| return; |
| } |
| mEnabled = enabled; |
| if (mEnabled) { |
| enable(); |
| } else { |
| disable(); |
| } |
| } |
| |
| @VisibleForTesting |
| void enable() { |
| // Needed since many Kids apps aren't optimised to support both orientations and it will be |
| // hard for kids to understand the app compat mode. |
| // TODO(229961548): Remove ignoreOrientationRequest exception for Kids Mode once possible. |
| if (mReverseDefaultRotationEnabled) { |
| setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled */ true, |
| /* fromOrientations */ |
| new int[]{SCREEN_ORIENTATION_LANDSCAPE, SCREEN_ORIENTATION_REVERSE_LANDSCAPE}, |
| /* toOrientations */ |
| new int[]{SCREEN_ORIENTATION_SENSOR_LANDSCAPE, |
| SCREEN_ORIENTATION_SENSOR_LANDSCAPE}); |
| } else { |
| setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled */ true, |
| /* fromOrientations */ null, /* toOrientations */ null); |
| } |
| 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<TaskAppearedInfo> 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() { |
| setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled */ false, |
| /* fromOrientations */ null, /* toOrientations */ null); |
| mDisplayInsetsController.removeInsetsChangedListener(DEFAULT_DISPLAY, |
| mOnInsetsChangedListener); |
| mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener); |
| updateTask(); |
| final WindowContainerToken token = mLaunchRootTask.token; |
| if (token != null) { |
| deleteRootTask(token); |
| } |
| mLaunchRootTask = null; |
| mLaunchRootLeash = null; |
| if (mHomeTask != null && mHomeTask.token != null) { |
| final WindowContainerToken homeToken = mHomeTask.token; |
| final WindowContainerTransaction wct = getWindowContainerTransaction(); |
| wct.setBounds(homeToken, null); |
| mSyncQueue.queue(wct); |
| } |
| mHomeTask = 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); |
| if (mEnabled) { |
| final SurfaceControl rootLeash = mLaunchRootLeash; |
| mSyncQueue.runInSync(t -> { |
| t.setPosition(rootLeash, taskBounds.left, taskBounds.top); |
| t.setWindowCrop(rootLeash, mDisplayWidth, mDisplayHeight); |
| }); |
| } |
| } |
| |
| private Rect calculateBounds() { |
| final Rect bounds = new Rect(0, 0, mDisplayWidth, mDisplayHeight); |
| bounds.inset(mInsetsState.calculateInsets( |
| bounds, WindowInsets.Type.navigationBars(), false /* ignoreVisibility */)); |
| return bounds; |
| } |
| |
| private void updateBounds() { |
| if (mLaunchRootTask == null) { |
| return; |
| } |
| final WindowContainerTransaction wct = getWindowContainerTransaction(); |
| final Rect taskBounds = calculateBounds(); |
| wct.setBounds(mLaunchRootTask.token, taskBounds); |
| wct.setBounds(mHomeTask.token, new Rect(0, 0, mDisplayWidth, mDisplayHeight)); |
| mSyncQueue.queue(wct); |
| final SurfaceControl finalLeash = mLaunchRootLeash; |
| mSyncQueue.runInSync(t -> { |
| t.setPosition(finalLeash, taskBounds.left, taskBounds.top); |
| t.setWindowCrop(finalLeash, mDisplayWidth, mDisplayHeight); |
| }); |
| } |
| |
| @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); |
| } |
| } |