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

Commit 8102e840 authored by Eric Lin's avatar Eric Lin
Browse files

Improve device state callback for thread context.

Optimize the initial device state callback execution in
DeviceStateManagerFoldingFeatureProducer (FFP) based on the calling
thread . If FFP is created on the main thread, execute the initial
`onDeviceStateChanged` callback directly. If FFP is created on a thread
other than the main thread, post the callback to the main thread.
This improves callback efficiency and maintains consistency across
thread contexts.

Bug: 337820752
Test: atest FrameworksCoreDeviceStateManagerTests:DeviceStateManagerGlobalTest
Test: atest WMJetpackUnitTests:DeviceStateManagerFoldingFeatureProducerTest
Flag: com.android.window.flags.wlinfo_oncreate
Change-Id: I3ee26a3427cf59c8da3366b48199e7703ec7c5e8
parent 320b9e02
Loading
Loading
Loading
Loading
+53 −34
Original line number Original line Diff line number Diff line
@@ -16,13 +16,17 @@


package android.hardware.devicestate;
package android.hardware.devicestate;


import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertThat;


import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;


@@ -40,7 +44,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import androidx.test.filters.SmallTest;


import com.android.internal.util.ConcurrentUtils;
import com.android.window.flags.Flags;
import com.android.window.flags.Flags;


import org.junit.Before;
import org.junit.Before;
@@ -52,6 +55,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashSet;
import java.util.List;
import java.util.List;
import java.util.Set;
import java.util.Set;
import java.util.concurrent.Executor;


import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
import platform.test.runner.parameterized.Parameters;
@@ -103,7 +107,7 @@ public final class DeviceStateManagerGlobalTest {
                permissionEnforcer, true /* simulatePostCallback */);
                permissionEnforcer, true /* simulatePostCallback */);
        final DeviceStateManagerGlobal dsmGlobal = new DeviceStateManagerGlobal(service);
        final DeviceStateManagerGlobal dsmGlobal = new DeviceStateManagerGlobal(service);
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        dsmGlobal.registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR);
        dsmGlobal.registerDeviceStateCallback(callback, DIRECT_EXECUTOR);


        verify(callback, never()).onDeviceStateChanged(any());
        verify(callback, never()).onDeviceStateChanged(any());


@@ -120,49 +124,68 @@ public final class DeviceStateManagerGlobalTest {
        final IDeviceStateManager service = new TestDeviceStateManagerService(permissionEnforcer);
        final IDeviceStateManager service = new TestDeviceStateManagerService(permissionEnforcer);
        final DeviceStateManagerGlobal dsmGlobal = new DeviceStateManagerGlobal(service);
        final DeviceStateManagerGlobal dsmGlobal = new DeviceStateManagerGlobal(service);
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        dsmGlobal.registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR);
        dsmGlobal.registerDeviceStateCallback(callback, DIRECT_EXECUTOR);


        verify(callback).onDeviceStateChanged(eq(DEFAULT_DEVICE_STATE));
        verify(callback).onDeviceStateChanged(eq(DEFAULT_DEVICE_STATE));
    }
    }


    @Test
    @Test
    public void registerCallback() {
    public void registerCallback_usesExecutorForCallbacks() {
        final DeviceStateCallback callback1 = mock(DeviceStateCallback.class);
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        final DeviceStateCallback callback2 = mock(DeviceStateCallback.class);
        final Executor executor = mock(Executor.class);
        doAnswer(invocation -> {
            Runnable runnable = (Runnable) invocation.getArguments()[0];
            runnable.run();
            return null;
        }).when(executor).execute(any(Runnable.class));


        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback1,
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback, executor);
                ConcurrentUtils.DIRECT_EXECUTOR);
        mService.setBaseState(OTHER_DEVICE_STATE);
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback2,
        mService.setSupportedStates(List.of(OTHER_DEVICE_STATE));
                ConcurrentUtils.DIRECT_EXECUTOR);


        // Verify initial callbacks
        // Verify that the given executor is used for both initial and subsequent callbacks.
        verify(callback1).onSupportedStatesChanged(eq(mService.getSupportedDeviceStates()));
        verify(executor, times(4)).execute(any(Runnable.class));
        verify(callback1).onDeviceStateChanged(eq(mService.getMergedState()));
    }
        verify(callback2).onDeviceStateChanged(eq(mService.getMergedState()));


        reset(callback1);
    @Test
        reset(callback2);
    public void registerCallback_supportedStatesChanged() {
        final DeviceStateCallback callback1 = mock(DeviceStateCallback.class);
        final DeviceStateCallback callback2 = mock(DeviceStateCallback.class);
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback1, DIRECT_EXECUTOR);
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback2, DIRECT_EXECUTOR);


        // Change the supported states and verify callback
        // Change the supported states and verify callback.
        mService.setSupportedStates(List.of(DEFAULT_DEVICE_STATE));
        mService.setSupportedStates(List.of(DEFAULT_DEVICE_STATE));

        verify(callback1).onSupportedStatesChanged(eq(mService.getSupportedDeviceStates()));
        verify(callback1).onSupportedStatesChanged(eq(mService.getSupportedDeviceStates()));
        verify(callback2).onSupportedStatesChanged(eq(mService.getSupportedDeviceStates()));
        verify(callback2).onSupportedStatesChanged(eq(mService.getSupportedDeviceStates()));
        mService.setSupportedStates(List.of(DEFAULT_DEVICE_STATE, OTHER_DEVICE_STATE));
    }


        reset(callback1);
    @Test
        reset(callback2);
    public void registerCallback_baseStateChanged() {
        final DeviceStateCallback callback1 = mock(DeviceStateCallback.class);
        final DeviceStateCallback callback2 = mock(DeviceStateCallback.class);
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback1, DIRECT_EXECUTOR);
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback2, DIRECT_EXECUTOR);
        mService.setSupportedStates(List.of(DEFAULT_DEVICE_STATE, OTHER_DEVICE_STATE));


        // Change the base state and verify callback
        // Change the base state and verify callback.
        mService.setBaseState(OTHER_DEVICE_STATE);
        mService.setBaseState(OTHER_DEVICE_STATE);

        verify(callback1).onDeviceStateChanged(eq(mService.getMergedState()));
        verify(callback1).onDeviceStateChanged(eq(mService.getMergedState()));
        verify(callback2).onDeviceStateChanged(eq(mService.getMergedState()));
        verify(callback2).onDeviceStateChanged(eq(mService.getMergedState()));
    }


        reset(callback1);
    @Test
        reset(callback2);
    public void registerCallback_requestedStateChanged() {

        final DeviceStateCallback callback1 = mock(DeviceStateCallback.class);
        // Change the requested state and verify callback
        final DeviceStateCallback callback2 = mock(DeviceStateCallback.class);
        final DeviceStateRequest request =
        final DeviceStateRequest request =
                DeviceStateRequest.newBuilder(DEFAULT_DEVICE_STATE.getIdentifier()).build();
                DeviceStateRequest.newBuilder(DEFAULT_DEVICE_STATE.getIdentifier()).build();
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback1, DIRECT_EXECUTOR);
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback2, DIRECT_EXECUTOR);

        // Change the requested state and verify callback.
        mDeviceStateManagerGlobal.requestState(request, null /* executor */, null /* callback */);
        mDeviceStateManagerGlobal.requestState(request, null /* executor */, null /* callback */);


        verify(callback1).onDeviceStateChanged(eq(mService.getMergedState()));
        verify(callback1).onDeviceStateChanged(eq(mService.getMergedState()));
@@ -173,8 +196,7 @@ public final class DeviceStateManagerGlobalTest {
    public void unregisterCallback() {
    public void unregisterCallback() {
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);


        mDeviceStateManagerGlobal
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback, DIRECT_EXECUTOR);
                .registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR);


        // Verify initial callbacks
        // Verify initial callbacks
        verify(callback).onSupportedStatesChanged(eq(mService.getSupportedDeviceStates()));
        verify(callback).onSupportedStatesChanged(eq(mService.getSupportedDeviceStates()));
@@ -191,8 +213,7 @@ public final class DeviceStateManagerGlobalTest {
    @Test
    @Test
    public void submitRequest() {
    public void submitRequest() {
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        mDeviceStateManagerGlobal
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback, DIRECT_EXECUTOR);
                .registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR);


        verify(callback).onDeviceStateChanged(eq(mService.getBaseState()));
        verify(callback).onDeviceStateChanged(eq(mService.getBaseState()));
        reset(callback);
        reset(callback);
@@ -212,8 +233,7 @@ public final class DeviceStateManagerGlobalTest {
    @Test
    @Test
    public void submitBaseStateOverrideRequest() {
    public void submitBaseStateOverrideRequest() {
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        mDeviceStateManagerGlobal
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback, DIRECT_EXECUTOR);
                .registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR);


        verify(callback).onDeviceStateChanged(eq(mService.getBaseState()));
        verify(callback).onDeviceStateChanged(eq(mService.getBaseState()));
        reset(callback);
        reset(callback);
@@ -234,8 +254,7 @@ public final class DeviceStateManagerGlobalTest {
    @Test
    @Test
    public void submitBaseAndEmulatedStateOverride() {
    public void submitBaseAndEmulatedStateOverride() {
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
        mDeviceStateManagerGlobal
        mDeviceStateManagerGlobal.registerDeviceStateCallback(callback, DIRECT_EXECUTOR);
                .registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR);


        verify(callback).onDeviceStateChanged(eq(mService.getBaseState()));
        verify(callback).onDeviceStateChanged(eq(mService.getBaseState()));
        reset(callback);
        reset(callback);
@@ -275,7 +294,7 @@ public final class DeviceStateManagerGlobalTest {
        final DeviceStateRequest request =
        final DeviceStateRequest request =
                DeviceStateRequest.newBuilder(OTHER_DEVICE_STATE.getIdentifier()).build();
                DeviceStateRequest.newBuilder(OTHER_DEVICE_STATE.getIdentifier()).build();
        mDeviceStateManagerGlobal.requestState(request,
        mDeviceStateManagerGlobal.requestState(request,
                ConcurrentUtils.DIRECT_EXECUTOR /* executor */,
                DIRECT_EXECUTOR /* executor */,
                callback /* callback */);
                callback /* callback */);


        verify(callback).onRequestActivated(eq(request));
        verify(callback).onRequestActivated(eq(request));
+23 −13
Original line number Original line Diff line number Diff line
@@ -26,10 +26,12 @@ import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
import android.hardware.devicestate.DeviceStateUtil;
import android.hardware.devicestate.DeviceStateUtil;
import android.os.Looper;
import android.text.TextUtils;
import android.text.TextUtils;
import android.util.Log;
import android.util.Log;
import android.util.SparseIntArray;
import android.util.SparseIntArray;


import androidx.annotation.BinderThread;
import androidx.annotation.MainThread;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.VisibleForTesting;
@@ -43,6 +45,7 @@ import java.util.Collections;
import java.util.List;
import java.util.List;
import java.util.Objects;
import java.util.Objects;
import java.util.Optional;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Consumer;


/**
/**
@@ -68,39 +71,49 @@ public final class DeviceStateManagerFoldingFeatureProducer
     */
     */
    private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE;
    private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE;


    @NonNull
    private final Context mContext;

    @NonNull
    @NonNull
    private final RawFoldingFeatureProducer mRawFoldSupplier;
    private final RawFoldingFeatureProducer mRawFoldSupplier;


    @NonNull
    @NonNull
    private final DeviceStateMapper mDeviceStateMapper;
    private final DeviceStateMapper mDeviceStateMapper;


    @VisibleForTesting
    private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
    final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
        @BinderThread // Subsequent callback after registered.
        // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the getData()
        @MainThread // Initial callback if registration is on the main thread.
        // implementation. See https://errorprone.info/bugpattern/GuardedBy for limitations.
        @SuppressWarnings("GuardedBy")
        @MainThread
        @Override
        @Override
        public void onDeviceStateChanged(@NonNull DeviceState state) {
        public void onDeviceStateChanged(@NonNull DeviceState state) {
            mCurrentDeviceState = state;
            final boolean isMainThread = Looper.myLooper() == Looper.getMainLooper();
            mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this
            final Executor executor = isMainThread ? Runnable::run : mContext.getMainExecutor();
                    ::notifyFoldingFeatureChangeLocked);
            executor.execute(() -> {
                DeviceStateManagerFoldingFeatureProducer.this.onDeviceStateChanged(state);
            });
        }
        }
    };
    };


    public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context,
    public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context,
            @NonNull RawFoldingFeatureProducer rawFoldSupplier,
            @NonNull RawFoldingFeatureProducer rawFoldSupplier,
            @NonNull DeviceStateManager deviceStateManager) {
            @NonNull DeviceStateManager deviceStateManager) {
        mContext = context;
        mRawFoldSupplier = rawFoldSupplier;
        mRawFoldSupplier = rawFoldSupplier;
        mDeviceStateMapper =
        mDeviceStateMapper =
                new DeviceStateMapper(context, deviceStateManager.getSupportedDeviceStates());
                new DeviceStateMapper(context, deviceStateManager.getSupportedDeviceStates());


        if (!mDeviceStateMapper.isDeviceStateToPostureMapEmpty()) {
        if (!mDeviceStateMapper.isDeviceStateToPostureMapEmpty()) {
            Objects.requireNonNull(deviceStateManager)
            Objects.requireNonNull(deviceStateManager)
                    .registerCallback(context.getMainExecutor(), mDeviceStateCallback);
                    .registerCallback(Runnable::run, mDeviceStateCallback);
        }
        }
    }
    }


    @MainThread
    @VisibleForTesting
    void onDeviceStateChanged(@NonNull DeviceState state) {
        mCurrentDeviceState = state;
        mRawFoldSupplier.getData(this::notifyFoldingFeatureChangeLocked);
    }

    /**
    /**
     * Add a callback to mCallbacks if there is no device state. This callback will be run
     * Add a callback to mCallbacks if there is no device state. This callback will be run
     * once a device state is set. Otherwise,run the callback immediately.
     * once a device state is set. Otherwise,run the callback immediately.
@@ -118,9 +131,6 @@ public final class DeviceStateManagerFoldingFeatureProducer
        }
        }
    }
    }


    // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the implementation of
    // addDataChangedCallback(). See https://errorprone.info/bugpattern/GuardedBy for limitations.
    @SuppressWarnings("GuardedBy")
    @Override
    @Override
    protected void onListenersChanged() {
    protected void onListenersChanged() {
        super.onListenersChanged();
        super.onListenersChanged();
+34 −7
Original line number Original line Diff line number Diff line
@@ -21,6 +21,7 @@ import android.content.res.Resources
import android.hardware.devicestate.DeviceState
import android.hardware.devicestate.DeviceState
import android.hardware.devicestate.DeviceStateManager
import android.hardware.devicestate.DeviceStateManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.window.common.layout.CommonFoldingFeature
import androidx.window.common.layout.CommonFoldingFeature
import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT
import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT
import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED
import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED
@@ -33,13 +34,14 @@ import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATU
import com.android.internal.R
import com.android.internal.R
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import java.util.Optional
import java.util.concurrent.Executor
import java.util.function.Consumer
import java.util.function.Consumer
import org.junit.Test
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.never
import org.mockito.kotlin.stub
import org.mockito.kotlin.stub
@@ -69,14 +71,39 @@ class DeviceStateManagerFoldingFeatureProducerTest {
    }
    }


    @Test
    @Test
    fun testRegisterCallback_usesMainExecutor() {
    fun testRegisterCallback_initialCallbackOnMainThread_executesDirectly() {
        DeviceStateManagerFoldingFeatureProducer(
        DeviceStateManagerFoldingFeatureProducer(
            mMockContext,
            mMockContext,
            mRawFoldSupplier,
            mRawFoldSupplier,
            mMockDeviceStateManager,
            mMockDeviceStateManager,
        )
        )
        val callbackCaptor = argumentCaptor<DeviceStateManager.DeviceStateCallback>()
        verify(mMockDeviceStateManager).registerCallback(any(), callbackCaptor.capture())


        verify(mMockDeviceStateManager).registerCallback(eq(mMockContext.mainExecutor), any())
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            callbackCaptor.firstValue.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
        }

        verify(mMockContext, never()).getMainExecutor()
    }

    @Test
    fun testRegisterCallback_subsequentCallbacks_postsToMainThread() {
        val mockMainExecutor = mock<Executor>()
        mMockContext.stub {
            on { getMainExecutor() } doReturn mockMainExecutor
        }
        DeviceStateManagerFoldingFeatureProducer(
            mMockContext,
            mRawFoldSupplier,
            mMockDeviceStateManager,
        )
        val callbackCaptor = argumentCaptor<DeviceStateManager.DeviceStateCallback>()
        verify(mMockDeviceStateManager).registerCallback(any(), callbackCaptor.capture())

        callbackCaptor.firstValue.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)

        verify(mockMainExecutor).execute(any())
    }
    }


    @Test
    @Test
@@ -86,7 +113,7 @@ class DeviceStateManagerFoldingFeatureProducerTest {
            mRawFoldSupplier,
            mRawFoldSupplier,
            mMockDeviceStateManager,
            mMockDeviceStateManager,
        )
        )
        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
        ffp.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)


        val currentData = ffp.getCurrentData()
        val currentData = ffp.getCurrentData()


@@ -209,7 +236,7 @@ class DeviceStateManagerFoldingFeatureProducerTest {
            mRawFoldSupplier,
            mRawFoldSupplier,
            mMockDeviceStateManager,
            mMockDeviceStateManager,
        )
        )
        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
        ffp.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
        val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
        val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()


        ffp.getData(storeFeaturesConsumer)
        ffp.getData(storeFeaturesConsumer)
@@ -229,8 +256,8 @@ class DeviceStateManagerFoldingFeatureProducerTest {
        ffp.getData(storeFeaturesConsumer)
        ffp.getData(storeFeaturesConsumer)


        verify(storeFeaturesConsumer, never()).accept(any())
        verify(storeFeaturesConsumer, never()).accept(any())
        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
        ffp.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
        ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_OPENED)
        ffp.onDeviceStateChanged(DEVICE_STATE_OPENED)
        verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
        verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
    }
    }