Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java +3 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,9 @@ import java.util.List; * * Note that most of the implementation here inherits from * {@link com.android.systemui.statusbar.policy.DevicePostureController}. * * Use the {@link TabletopModeController} if you are interested in tabletop mode change only, * which is more common. */ public class DevicePostureController { @IntDef(prefix = {"DEVICE_POSTURE_"}, value = { Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java 0 → 100644 +208 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.common; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FOLDABLE; import android.annotation.NonNull; import android.app.WindowConfiguration; import android.content.Context; import android.content.res.Configuration; import android.util.ArraySet; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * Wrapper class to track the tabletop (aka. flex) mode change on Fold-ables. * See also <a * href="https://developer.android.com/guide/topics/large-screens/learn-about-foldables * #foldable_postures">Foldable states and postures</a> for reference. * * Use the {@link DevicePostureController} for more detailed posture changes. */ public class TabletopModeController implements DevicePostureController.OnDevicePostureChangedListener, DisplayController.OnDisplaysChangedListener { private static final long TABLETOP_MODE_DELAY_MILLIS = 1_000; private final Context mContext; private final DevicePostureController mDevicePostureController; private final DisplayController mDisplayController; private final ShellExecutor mMainExecutor; private final Set<Integer> mTabletopModeRotations = new ArraySet<>(); private final List<OnTabletopModeChangedListener> mListeners = new ArrayList<>(); @VisibleForTesting final Runnable mOnEnterTabletopModeCallback = () -> { if (isInTabletopMode()) { // We are still in tabletop mode, go ahead. mayBroadcastOnTabletopModeChange(true /* isInTabletopMode */); } }; @DevicePostureController.DevicePostureInt private int mDevicePosture = DEVICE_POSTURE_UNKNOWN; @Surface.Rotation private int mDisplayRotation = WindowConfiguration.ROTATION_UNDEFINED; /** * Track the last callback value for {@link OnTabletopModeChangedListener}. * This is to avoid duplicated {@code false} callback to {@link #mListeners}. */ private Boolean mLastIsInTabletopModeForCallback; public TabletopModeController(Context context, ShellInit shellInit, DevicePostureController postureController, DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor) { mContext = context; mDevicePostureController = postureController; mDisplayController = displayController; mMainExecutor = mainExecutor; shellInit.addInitCallback(this::onInit, this); } @VisibleForTesting void onInit() { mDevicePostureController.registerOnDevicePostureChangedListener(this); mDisplayController.addDisplayWindowListener(this); // Aligns with what's in {@link com.android.server.wm.DisplayRotation}. final int[] deviceTabletopRotations = mContext.getResources().getIntArray( com.android.internal.R.array.config_deviceTabletopRotations); if (deviceTabletopRotations == null || deviceTabletopRotations.length == 0) { ProtoLog.e(WM_SHELL_FOLDABLE, "No valid config_deviceTabletopRotations, can not tell" + " tabletop mode in WMShell"); return; } for (int angle : deviceTabletopRotations) { switch (angle) { case 0: mTabletopModeRotations.add(Surface.ROTATION_0); break; case 90: mTabletopModeRotations.add(Surface.ROTATION_90); break; case 180: mTabletopModeRotations.add(Surface.ROTATION_180); break; case 270: mTabletopModeRotations.add(Surface.ROTATION_270); break; default: ProtoLog.e(WM_SHELL_FOLDABLE, "Invalid surface rotation angle in " + "config_deviceTabletopRotations: %d", angle); break; } } } /** Register {@link OnTabletopModeChangedListener} to listen for tabletop mode change. */ public void registerOnTabletopModeChangedListener( @NonNull OnTabletopModeChangedListener listener) { if (listener == null || mListeners.contains(listener)) return; mListeners.add(listener); listener.onTabletopModeChanged(isInTabletopMode()); } /** Unregister {@link OnTabletopModeChangedListener} for tabletop mode change. */ public void unregisterOnTabletopModeChangedListener( @NonNull OnTabletopModeChangedListener listener) { mListeners.remove(listener); } @Override public void onDevicePostureChanged(@DevicePostureController.DevicePostureInt int posture) { if (mDevicePosture != posture) { onDevicePostureOrDisplayRotationChanged(posture, mDisplayRotation); } } @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { final int newDisplayRotation = newConfig.windowConfiguration.getDisplayRotation(); if (displayId == DEFAULT_DISPLAY && newDisplayRotation != mDisplayRotation) { onDevicePostureOrDisplayRotationChanged(mDevicePosture, newDisplayRotation); } } private void onDevicePostureOrDisplayRotationChanged( @DevicePostureController.DevicePostureInt int newPosture, @Surface.Rotation int newDisplayRotation) { final boolean wasInTabletopMode = isInTabletopMode(); mDevicePosture = newPosture; mDisplayRotation = newDisplayRotation; final boolean couldBeInTabletopMode = isInTabletopMode(); mMainExecutor.removeCallbacks(mOnEnterTabletopModeCallback); if (!wasInTabletopMode && couldBeInTabletopMode) { // May enter tabletop mode, but we need to wait for additional time since this // could be an intermediate state. mMainExecutor.executeDelayed(mOnEnterTabletopModeCallback, TABLETOP_MODE_DELAY_MILLIS); } else { // Cancel entering tabletop mode if any condition's changed. mayBroadcastOnTabletopModeChange(false /* isInTabletopMode */); } } private boolean isHalfOpened(@DevicePostureController.DevicePostureInt int posture) { return posture == DEVICE_POSTURE_HALF_OPENED; } private boolean isInTabletopMode() { return isHalfOpened(mDevicePosture) && mTabletopModeRotations.contains(mDisplayRotation); } private void mayBroadcastOnTabletopModeChange(boolean isInTabletopMode) { if (mLastIsInTabletopModeForCallback == null || mLastIsInTabletopModeForCallback != isInTabletopMode) { mListeners.forEach(l -> l.onTabletopModeChanged(isInTabletopMode)); mLastIsInTabletopModeForCallback = isInTabletopMode; } } /** * Listener interface for tabletop mode change. */ public interface OnTabletopModeChangedListener { /** * Callback when tabletop mode changes. Expect duplicated callbacks with {@code false}. * @param isInTabletopMode {@code true} if enters tabletop mode, {@code false} otherwise. */ void onTabletopModeChanged(boolean isInTabletopMode); } } libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +13 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellAnimationThread; Loading Loading @@ -169,6 +170,18 @@ public abstract class WMShellBaseModule { return new DevicePostureController(context, shellInit, mainExecutor); } @WMSingleton @Provides static TabletopModeController provideTabletopModeController( Context context, ShellInit shellInit, DevicePostureController postureController, DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor) { return new TabletopModeController( context, shellInit, postureController, displayController, mainExecutor); } @WMSingleton @Provides static DragAndDropController provideDragAndDropController(Context context, Loading libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +2 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java 0 → 100644 +270 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.common; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_CLOSED; import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_OPENED; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.Surface; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** * Tests for {@link TabletopModeController}. */ @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @SmallTest public class TabletopModeControllerTest extends ShellTestCase { // It's considered tabletop mode if the display rotation angle matches what's in this array. // It's defined as com.android.internal.R.array.config_deviceTabletopRotations on real devices. private static final int[] TABLETOP_MODE_ROTATIONS = new int[] { 90 /* Surface.ROTATION_90 */, 270 /* Surface.ROTATION_270 */ }; private TestShellExecutor mMainExecutor; private Configuration mConfiguration; private TabletopModeController mPipTabletopController; @Mock private Context mContext; @Mock private ShellInit mShellInit; @Mock private Resources mResources; @Mock private DevicePostureController mDevicePostureController; @Mock private DisplayController mDisplayController; @Mock private TabletopModeController.OnTabletopModeChangedListener mOnTabletopModeChangedListener; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(mResources.getIntArray(com.android.internal.R.array.config_deviceTabletopRotations)) .thenReturn(TABLETOP_MODE_ROTATIONS); when(mContext.getResources()).thenReturn(mResources); mMainExecutor = new TestShellExecutor(); mConfiguration = new Configuration(); mPipTabletopController = new TabletopModeController(mContext, mShellInit, mDevicePostureController, mDisplayController, mMainExecutor); mPipTabletopController.onInit(); } @Test public void instantiateController_addInitCallback() { verify(mShellInit, times(1)).addInitCallback(any(), eq(mPipTabletopController)); } @Test public void registerOnTabletopModeChangedListener_notInTabletopMode_callbackFalse() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.registerOnTabletopModeChangedListener( mOnTabletopModeChangedListener); verify(mOnTabletopModeChangedListener, times(1)) .onTabletopModeChanged(false); } @Test public void registerOnTabletopModeChangedListener_inTabletopMode_callbackTrue() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.registerOnTabletopModeChangedListener( mOnTabletopModeChangedListener); verify(mOnTabletopModeChangedListener, times(1)) .onTabletopModeChanged(true); } @Test public void registerOnTabletopModeChangedListener_notInTabletopModeTwice_callbackOnce() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.registerOnTabletopModeChangedListener( mOnTabletopModeChangedListener); clearInvocations(mOnTabletopModeChangedListener); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); verifyZeroInteractions(mOnTabletopModeChangedListener); } // Test cases starting from folded state (DEVICE_POSTURE_CLOSED) @Test public void foldedRotation90_halfOpen_scheduleTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void foldedRotation0_halfOpen_noScheduleTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void foldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void foldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void foldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } // Test cases starting from unfolded state (DEVICE_POSTURE_OPENED) @Test public void unfoldedRotation90_halfOpen_scheduleTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void unfoldedRotation0_halfOpen_noScheduleTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void unfoldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void unfoldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void unfoldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java +3 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,9 @@ import java.util.List; * * Note that most of the implementation here inherits from * {@link com.android.systemui.statusbar.policy.DevicePostureController}. * * Use the {@link TabletopModeController} if you are interested in tabletop mode change only, * which is more common. */ public class DevicePostureController { @IntDef(prefix = {"DEVICE_POSTURE_"}, value = { Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java 0 → 100644 +208 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.common; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FOLDABLE; import android.annotation.NonNull; import android.app.WindowConfiguration; import android.content.Context; import android.content.res.Configuration; import android.util.ArraySet; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * Wrapper class to track the tabletop (aka. flex) mode change on Fold-ables. * See also <a * href="https://developer.android.com/guide/topics/large-screens/learn-about-foldables * #foldable_postures">Foldable states and postures</a> for reference. * * Use the {@link DevicePostureController} for more detailed posture changes. */ public class TabletopModeController implements DevicePostureController.OnDevicePostureChangedListener, DisplayController.OnDisplaysChangedListener { private static final long TABLETOP_MODE_DELAY_MILLIS = 1_000; private final Context mContext; private final DevicePostureController mDevicePostureController; private final DisplayController mDisplayController; private final ShellExecutor mMainExecutor; private final Set<Integer> mTabletopModeRotations = new ArraySet<>(); private final List<OnTabletopModeChangedListener> mListeners = new ArrayList<>(); @VisibleForTesting final Runnable mOnEnterTabletopModeCallback = () -> { if (isInTabletopMode()) { // We are still in tabletop mode, go ahead. mayBroadcastOnTabletopModeChange(true /* isInTabletopMode */); } }; @DevicePostureController.DevicePostureInt private int mDevicePosture = DEVICE_POSTURE_UNKNOWN; @Surface.Rotation private int mDisplayRotation = WindowConfiguration.ROTATION_UNDEFINED; /** * Track the last callback value for {@link OnTabletopModeChangedListener}. * This is to avoid duplicated {@code false} callback to {@link #mListeners}. */ private Boolean mLastIsInTabletopModeForCallback; public TabletopModeController(Context context, ShellInit shellInit, DevicePostureController postureController, DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor) { mContext = context; mDevicePostureController = postureController; mDisplayController = displayController; mMainExecutor = mainExecutor; shellInit.addInitCallback(this::onInit, this); } @VisibleForTesting void onInit() { mDevicePostureController.registerOnDevicePostureChangedListener(this); mDisplayController.addDisplayWindowListener(this); // Aligns with what's in {@link com.android.server.wm.DisplayRotation}. final int[] deviceTabletopRotations = mContext.getResources().getIntArray( com.android.internal.R.array.config_deviceTabletopRotations); if (deviceTabletopRotations == null || deviceTabletopRotations.length == 0) { ProtoLog.e(WM_SHELL_FOLDABLE, "No valid config_deviceTabletopRotations, can not tell" + " tabletop mode in WMShell"); return; } for (int angle : deviceTabletopRotations) { switch (angle) { case 0: mTabletopModeRotations.add(Surface.ROTATION_0); break; case 90: mTabletopModeRotations.add(Surface.ROTATION_90); break; case 180: mTabletopModeRotations.add(Surface.ROTATION_180); break; case 270: mTabletopModeRotations.add(Surface.ROTATION_270); break; default: ProtoLog.e(WM_SHELL_FOLDABLE, "Invalid surface rotation angle in " + "config_deviceTabletopRotations: %d", angle); break; } } } /** Register {@link OnTabletopModeChangedListener} to listen for tabletop mode change. */ public void registerOnTabletopModeChangedListener( @NonNull OnTabletopModeChangedListener listener) { if (listener == null || mListeners.contains(listener)) return; mListeners.add(listener); listener.onTabletopModeChanged(isInTabletopMode()); } /** Unregister {@link OnTabletopModeChangedListener} for tabletop mode change. */ public void unregisterOnTabletopModeChangedListener( @NonNull OnTabletopModeChangedListener listener) { mListeners.remove(listener); } @Override public void onDevicePostureChanged(@DevicePostureController.DevicePostureInt int posture) { if (mDevicePosture != posture) { onDevicePostureOrDisplayRotationChanged(posture, mDisplayRotation); } } @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { final int newDisplayRotation = newConfig.windowConfiguration.getDisplayRotation(); if (displayId == DEFAULT_DISPLAY && newDisplayRotation != mDisplayRotation) { onDevicePostureOrDisplayRotationChanged(mDevicePosture, newDisplayRotation); } } private void onDevicePostureOrDisplayRotationChanged( @DevicePostureController.DevicePostureInt int newPosture, @Surface.Rotation int newDisplayRotation) { final boolean wasInTabletopMode = isInTabletopMode(); mDevicePosture = newPosture; mDisplayRotation = newDisplayRotation; final boolean couldBeInTabletopMode = isInTabletopMode(); mMainExecutor.removeCallbacks(mOnEnterTabletopModeCallback); if (!wasInTabletopMode && couldBeInTabletopMode) { // May enter tabletop mode, but we need to wait for additional time since this // could be an intermediate state. mMainExecutor.executeDelayed(mOnEnterTabletopModeCallback, TABLETOP_MODE_DELAY_MILLIS); } else { // Cancel entering tabletop mode if any condition's changed. mayBroadcastOnTabletopModeChange(false /* isInTabletopMode */); } } private boolean isHalfOpened(@DevicePostureController.DevicePostureInt int posture) { return posture == DEVICE_POSTURE_HALF_OPENED; } private boolean isInTabletopMode() { return isHalfOpened(mDevicePosture) && mTabletopModeRotations.contains(mDisplayRotation); } private void mayBroadcastOnTabletopModeChange(boolean isInTabletopMode) { if (mLastIsInTabletopModeForCallback == null || mLastIsInTabletopModeForCallback != isInTabletopMode) { mListeners.forEach(l -> l.onTabletopModeChanged(isInTabletopMode)); mLastIsInTabletopModeForCallback = isInTabletopMode; } } /** * Listener interface for tabletop mode change. */ public interface OnTabletopModeChangedListener { /** * Callback when tabletop mode changes. Expect duplicated callbacks with {@code false}. * @param isInTabletopMode {@code true} if enters tabletop mode, {@code false} otherwise. */ void onTabletopModeChanged(boolean isInTabletopMode); } }
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +13 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellAnimationThread; Loading Loading @@ -169,6 +170,18 @@ public abstract class WMShellBaseModule { return new DevicePostureController(context, shellInit, mainExecutor); } @WMSingleton @Provides static TabletopModeController provideTabletopModeController( Context context, ShellInit shellInit, DevicePostureController postureController, DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor) { return new TabletopModeController( context, shellInit, postureController, displayController, mainExecutor); } @WMSingleton @Provides static DragAndDropController provideDragAndDropController(Context context, Loading
libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +2 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java 0 → 100644 +270 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.common; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_CLOSED; import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_OPENED; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.Surface; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** * Tests for {@link TabletopModeController}. */ @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @SmallTest public class TabletopModeControllerTest extends ShellTestCase { // It's considered tabletop mode if the display rotation angle matches what's in this array. // It's defined as com.android.internal.R.array.config_deviceTabletopRotations on real devices. private static final int[] TABLETOP_MODE_ROTATIONS = new int[] { 90 /* Surface.ROTATION_90 */, 270 /* Surface.ROTATION_270 */ }; private TestShellExecutor mMainExecutor; private Configuration mConfiguration; private TabletopModeController mPipTabletopController; @Mock private Context mContext; @Mock private ShellInit mShellInit; @Mock private Resources mResources; @Mock private DevicePostureController mDevicePostureController; @Mock private DisplayController mDisplayController; @Mock private TabletopModeController.OnTabletopModeChangedListener mOnTabletopModeChangedListener; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(mResources.getIntArray(com.android.internal.R.array.config_deviceTabletopRotations)) .thenReturn(TABLETOP_MODE_ROTATIONS); when(mContext.getResources()).thenReturn(mResources); mMainExecutor = new TestShellExecutor(); mConfiguration = new Configuration(); mPipTabletopController = new TabletopModeController(mContext, mShellInit, mDevicePostureController, mDisplayController, mMainExecutor); mPipTabletopController.onInit(); } @Test public void instantiateController_addInitCallback() { verify(mShellInit, times(1)).addInitCallback(any(), eq(mPipTabletopController)); } @Test public void registerOnTabletopModeChangedListener_notInTabletopMode_callbackFalse() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.registerOnTabletopModeChangedListener( mOnTabletopModeChangedListener); verify(mOnTabletopModeChangedListener, times(1)) .onTabletopModeChanged(false); } @Test public void registerOnTabletopModeChangedListener_inTabletopMode_callbackTrue() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.registerOnTabletopModeChangedListener( mOnTabletopModeChangedListener); verify(mOnTabletopModeChangedListener, times(1)) .onTabletopModeChanged(true); } @Test public void registerOnTabletopModeChangedListener_notInTabletopModeTwice_callbackOnce() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.registerOnTabletopModeChangedListener( mOnTabletopModeChangedListener); clearInvocations(mOnTabletopModeChangedListener); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); verifyZeroInteractions(mOnTabletopModeChangedListener); } // Test cases starting from folded state (DEVICE_POSTURE_CLOSED) @Test public void foldedRotation90_halfOpen_scheduleTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void foldedRotation0_halfOpen_noScheduleTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void foldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void foldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void foldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } // Test cases starting from unfolded state (DEVICE_POSTURE_OPENED) @Test public void unfoldedRotation90_halfOpen_scheduleTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void unfoldedRotation0_halfOpen_noScheduleTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void unfoldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void unfoldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } @Test public void unfoldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); } }