Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 47bbbd75 authored by Hongwei Wang's avatar Hongwei Wang Committed by Automerger Merge Worker
Browse files

Merge "Add TabletopModeController in WMShell" into tm-qpr-dev am: fb933d46...

Merge "Add TabletopModeController in WMShell" into tm-qpr-dev am: fb933d46 am: 3c7723fc am: 7364292a

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21695383



Change-Id: Ibde37a44311931efe685c7afb6e9a8d3fe7a192d
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents f86cc7ad 7364292a
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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 = {
+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);
    }
}
+13 −0
Original line number Diff line number Diff line
@@ -52,6 +52,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;
@@ -170,6 +171,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,
+2 −0
Original line number Diff line number Diff line
@@ -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;
+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));
    }
}