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

Commit 52845754 authored by Hongwei Wang's avatar Hongwei Wang
Browse files

Add DevicePostureController for WMShell

This can be used for WMShell component who is interested in the foldable
state, such as OPEN / CLOSE / HALF_OPEN.

When device is in HALF_OPEN state, one can use the rotation state to
further differentiate tabletop (horizontal fold) or book (vertical fold)
posture.

See also:
https://developer.android.com/guide/topics/large-screens/learn-about-foldables#foldable_postures

Bug: 260871991
Test: atest WMShellUnitTests:DevicePostureControllerTest
Change-Id: I8fd7c0e296db4abbddbd9131bf18bd541b7b9944
parent 82dc9cb0
Loading
Loading
Loading
Loading
+147 −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 android.annotation.IntDef;
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.util.SparseIntArray;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.sysui.ShellInit;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

/**
 * Wrapper class to track the device posture 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.
 *
 * Note that most of the implementation here inherits from
 * {@link com.android.systemui.statusbar.policy.DevicePostureController}.
 */
public class DevicePostureController {
    @IntDef(prefix = {"DEVICE_POSTURE_"}, value = {
            DEVICE_POSTURE_UNKNOWN,
            DEVICE_POSTURE_CLOSED,
            DEVICE_POSTURE_HALF_OPENED,
            DEVICE_POSTURE_OPENED,
            DEVICE_POSTURE_FLIPPED
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface DevicePostureInt {}

    // NOTE: These constants **must** match those defined for Jetpack Sidecar. This is because we
    // use the Device State -> Jetpack Posture map to translate between the two.
    public static final int DEVICE_POSTURE_UNKNOWN = 0;
    public static final int DEVICE_POSTURE_CLOSED = 1;
    public static final int DEVICE_POSTURE_HALF_OPENED = 2;
    public static final int DEVICE_POSTURE_OPENED = 3;
    public static final int DEVICE_POSTURE_FLIPPED = 4;

    private final Context mContext;
    private final ShellExecutor mMainExecutor;
    private final List<OnDevicePostureChangedListener> mListeners = new ArrayList<>();
    private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();

    private int mDevicePosture = DEVICE_POSTURE_UNKNOWN;

    public DevicePostureController(
            Context context, ShellInit shellInit, ShellExecutor mainExecutor) {
        mContext = context;
        mMainExecutor = mainExecutor;
        shellInit.addInitCallback(this::onInit, this);
    }

    private void onInit() {
        // Most of this is borrowed from WindowManager/Jetpack/DeviceStateManagerPostureProducer.
        // Using the sidecar/extension libraries directly brings in a new dependency that it'd be
        // good to avoid (along with the fact that sidecar is deprecated, and extensions isn't fully
        // ready yet), and we'd have to make our own layer over the sidecar library anyway to easily
        // allow the implementation to change, so it was easier to just interface with
        // DeviceStateManager directly.
        String[] deviceStatePosturePairs = mContext.getResources()
                .getStringArray(R.array.config_device_state_postures);
        for (String deviceStatePosturePair : deviceStatePosturePairs) {
            String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
            if (deviceStatePostureMapping.length != 2) {
                continue;
            }

            int deviceState;
            int posture;
            try {
                deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
                posture = Integer.parseInt(deviceStatePostureMapping[1]);
            } catch (NumberFormatException e) {
                continue;
            }

            mDeviceStateToPostureMap.put(deviceState, posture);
        }

        final DeviceStateManager deviceStateManager = mContext.getSystemService(
                DeviceStateManager.class);
        if (deviceStateManager != null) {
            deviceStateManager.registerCallback(mMainExecutor, state -> onDevicePostureChanged(
                    mDeviceStateToPostureMap.get(state, DEVICE_POSTURE_UNKNOWN)));
        }
    }

    @VisibleForTesting
    void onDevicePostureChanged(int devicePosture) {
        if (devicePosture == mDevicePosture) return;
        mDevicePosture = devicePosture;
        mListeners.forEach(l -> l.onDevicePostureChanged(mDevicePosture));
    }

    /**
     * Register {@link OnDevicePostureChangedListener} for device posture changes.
     * The listener will receive callback with current device posture upon registration.
     */
    public void registerOnDevicePostureChangedListener(
            @NonNull OnDevicePostureChangedListener listener) {
        if (mListeners.contains(listener)) return;
        mListeners.add(listener);
        listener.onDevicePostureChanged(mDevicePosture);
    }

    /**
     * Unregister {@link OnDevicePostureChangedListener} for device posture changes.
     */
    public void unregisterOnDevicePostureChangedListener(
            @NonNull OnDevicePostureChangedListener listener) {
        mListeners.remove(listener);
    }

    /**
     * Listener interface for device posture change.
     */
    public interface OnDevicePostureChangedListener {
        /**
         * Callback when device posture changes.
         * See {@link DevicePostureInt} for callback values.
         */
        void onDevicePostureChanged(@DevicePostureInt int posture);
    }
}
+11 −0
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.back.BackAnimationController;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.common.DevicePostureController;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -158,6 +159,16 @@ public abstract class WMShellBaseModule {
        return new DisplayLayout();
    }

    @WMSingleton
    @Provides
    static DevicePostureController provideDevicePostureController(
            Context context,
            ShellInit shellInit,
            @ShellMainThread ShellExecutor mainExecutor
    ) {
        return new DevicePostureController(context, shellInit, mainExecutor);
    }

    @WMSingleton
    @Provides
    static DragAndDropController provideDragAndDropController(Context context,
+109 −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 org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
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 android.content.Context;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.wm.shell.sysui.ShellInit;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class DevicePostureControllerTest {
    @Mock
    private Context mContext;

    @Mock
    private ShellInit mShellInit;

    @Mock
    private ShellExecutor mMainExecutor;

    @Captor
    private ArgumentCaptor<Integer> mDevicePostureCaptor;

    @Mock
    private DevicePostureController.OnDevicePostureChangedListener mOnDevicePostureChangedListener;

    private DevicePostureController mDevicePostureController;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mDevicePostureController = new DevicePostureController(mContext, mShellInit, mMainExecutor);
    }

    @Test
    public void instantiateController_addInitCallback() {
        verify(mShellInit, times(1)).addInitCallback(any(), eq(mDevicePostureController));
    }

    @Test
    public void registerOnDevicePostureChangedListener_callbackCurrentPosture() {
        mDevicePostureController.registerOnDevicePostureChangedListener(
                mOnDevicePostureChangedListener);
        verify(mOnDevicePostureChangedListener, times(1))
                .onDevicePostureChanged(anyInt());
    }

    @Test
    public void onDevicePostureChanged_differentPosture_callbackListener() {
        mDevicePostureController.registerOnDevicePostureChangedListener(
                mOnDevicePostureChangedListener);
        verify(mOnDevicePostureChangedListener).onDevicePostureChanged(
                mDevicePostureCaptor.capture());
        clearInvocations(mOnDevicePostureChangedListener);

        int differentDevicePosture = mDevicePostureCaptor.getValue() + 1;
        mDevicePostureController.onDevicePostureChanged(differentDevicePosture);

        verify(mOnDevicePostureChangedListener, times(1))
                .onDevicePostureChanged(differentDevicePosture);
    }

    @Test
    public void onDevicePostureChanged_samePosture_doesNotCallbackListener() {
        mDevicePostureController.registerOnDevicePostureChangedListener(
                mOnDevicePostureChangedListener);
        verify(mOnDevicePostureChangedListener).onDevicePostureChanged(
                mDevicePostureCaptor.capture());
        clearInvocations(mOnDevicePostureChangedListener);

        int sameDevicePosture = mDevicePostureCaptor.getValue();
        mDevicePostureController.onDevicePostureChanged(sameDevicePosture);

        verifyZeroInteractions(mOnDevicePostureChangedListener);
    }
}