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

Commit fda0b768 authored by Automerger Merge Worker's avatar Automerger Merge Worker Committed by Android (Google) Code Review
Browse files

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

Merge "Merge "Add TabletopModeController in WMShell" into tm-qpr-dev am: fb933d46 am: 3c7723fc am: 7364292a am: 47bbbd75"
parents fa66319d 13d992fe
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));
    }
}