summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java270
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/Android.bp2
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt341
3 files changed, 515 insertions, 98 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index 37f0067de453..089613853555 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -16,12 +16,11 @@
package androidx.window.common;
-import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString;
-import android.annotation.NonNull;
import android.content.Context;
import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateManager;
@@ -31,16 +30,23 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;
+import androidx.annotation.BinderThread;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import androidx.window.common.layout.CommonFoldingFeature;
import androidx.window.common.layout.DisplayFoldFeatureCommon;
import com.android.internal.R;
+import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
@@ -55,13 +61,6 @@ public final class DeviceStateManagerFoldingFeatureProducer
private static final boolean DEBUG = false;
/**
- * Emulated device state
- * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
- * {@link CommonFoldingFeature.State} map.
- */
- private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
-
- /**
* Device state received via
* {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}.
* The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1
@@ -71,23 +70,40 @@ public final class DeviceStateManagerFoldingFeatureProducer
* "rear display". Concurrent mode for example is activated via public API and can be active in
* both the "open" and "half folded" device states.
*/
- private DeviceState mCurrentDeviceState = new DeviceState(
- new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
- "INVALID").build());
+ // TODO: b/337820752 - Add @GuardedBy("mCurrentDeviceStateLock") after flag cleanup.
+ private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE;
- private List<DeviceState> mSupportedStates;
+ /**
+ * Lock to synchronize access to {@link #mCurrentDeviceState}.
+ *
+ * <p>This lock is used to ensure thread-safety when accessing and modifying the
+ * {@link #mCurrentDeviceState} field. It is acquired by both the binder thread (if
+ * {@link Flags#wlinfoOncreate()} is enabled) and the main thread (if
+ * {@link Flags#wlinfoOncreate()} is disabled) to prevent race conditions and
+ * ensure data consistency.
+ */
+ private final Object mCurrentDeviceStateLock = new Object();
@NonNull
private final RawFoldingFeatureProducer mRawFoldSupplier;
- private final boolean mIsHalfOpenedSupported;
-
- private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+ @NonNull
+ private final DeviceStateMapper mDeviceStateMapper;
+
+ @VisibleForTesting
+ final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+ // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the getData()
+ // implementation. See https://errorprone.info/bugpattern/GuardedBy for limitations.
+ @SuppressWarnings("GuardedBy")
+ @BinderThread // When Flags.wlinfoOncreate() is enabled.
+ @MainThread // When Flags.wlinfoOncreate() is disabled.
@Override
public void onDeviceStateChanged(@NonNull DeviceState state) {
- mCurrentDeviceState = state;
- mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
- .this::notifyFoldingFeatureChange);
+ synchronized (mCurrentDeviceStateLock) {
+ mCurrentDeviceState = state;
+ mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this
+ ::notifyFoldingFeatureChangeLocked);
+ }
}
};
@@ -95,41 +111,14 @@ public final class DeviceStateManagerFoldingFeatureProducer
@NonNull RawFoldingFeatureProducer rawFoldSupplier,
@NonNull DeviceStateManager deviceStateManager) {
mRawFoldSupplier = rawFoldSupplier;
- String[] deviceStatePosturePairs = context.getResources()
- .getStringArray(R.array.config_device_state_postures);
- mSupportedStates = deviceStateManager.getSupportedDeviceStates();
- boolean isHalfOpenedSupported = false;
- for (String deviceStatePosturePair : deviceStatePosturePairs) {
- String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
- if (deviceStatePostureMapping.length != 2) {
- if (DEBUG) {
- Log.e(TAG, "Malformed device state posture pair: "
- + deviceStatePosturePair);
- }
- continue;
- }
+ mDeviceStateMapper =
+ new DeviceStateMapper(context, deviceStateManager.getSupportedDeviceStates());
- int deviceState;
- int posture;
- try {
- deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
- posture = Integer.parseInt(deviceStatePostureMapping[1]);
- } catch (NumberFormatException e) {
- if (DEBUG) {
- Log.e(TAG, "Failed to parse device state or posture: "
- + deviceStatePosturePair,
- e);
- }
- continue;
- }
- isHalfOpenedSupported = isHalfOpenedSupported
- || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
- mDeviceStateToPostureMap.put(deviceState, posture);
- }
- mIsHalfOpenedSupported = isHalfOpenedSupported;
- if (mDeviceStateToPostureMap.size() > 0) {
+ if (!mDeviceStateMapper.isDeviceStateToPostureMapEmpty()) {
+ final Executor executor =
+ Flags.wlinfoOncreate() ? Runnable::run : context.getMainExecutor();
Objects.requireNonNull(deviceStateManager)
- .registerCallback(context.getMainExecutor(), mDeviceStateCallback);
+ .registerCallback(executor, mDeviceStateCallback);
}
}
@@ -137,50 +126,51 @@ public final class DeviceStateManagerFoldingFeatureProducer
* 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.
*/
- private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback,
- String displayFeaturesString) {
- if (isCurrentStateValid()) {
- callback.accept(calculateFoldingFeature(displayFeaturesString));
+ private void runCallbackWhenValidState(@NonNull DeviceState state,
+ @NonNull Consumer<List<CommonFoldingFeature>> callback,
+ @NonNull String displayFeaturesString) {
+ if (mDeviceStateMapper.isDeviceStateValid(state)) {
+ callback.accept(calculateFoldingFeature(state, displayFeaturesString));
} else {
// This callback will be added to mCallbacks and removed once it runs once.
- AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
+ final AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
new AcceptOnceConsumer<>(this, callback);
addDataChangedCallback(singleRunCallback);
}
}
- /**
- * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the
- * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was
- * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}.
- * Returns a boolean value of whether the device state is valid.
- */
- private boolean isCurrentStateValid() {
- // If the device state is not found in the map, indexOfKey returns a negative number.
- return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0;
- }
-
+ // 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
protected void onListenersChanged() {
super.onListenersChanged();
- if (hasListeners()) {
- mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
- } else {
- mCurrentDeviceState = new DeviceState(
- new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
- "INVALID").build());
- mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
+ synchronized (mCurrentDeviceStateLock) {
+ if (hasListeners()) {
+ mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChangeLocked);
+ } else {
+ mCurrentDeviceState = INVALID_DEVICE_STATE;
+ mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChangeLocked);
+ }
+ }
+ }
+
+ @NonNull
+ private DeviceState getCurrentDeviceState() {
+ synchronized (mCurrentDeviceStateLock) {
+ return mCurrentDeviceState;
}
}
@NonNull
@Override
public Optional<List<CommonFoldingFeature>> getCurrentData() {
- Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
- if (!isCurrentStateValid()) {
+ final Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
+ final DeviceState state = getCurrentDeviceState();
+ if (!mDeviceStateMapper.isDeviceStateValid(state) || displayFeaturesString.isEmpty()) {
return Optional.empty();
} else {
- return displayFeaturesString.map(this::calculateFoldingFeature);
+ return Optional.of(calculateFoldingFeature(state, displayFeaturesString.get()));
}
}
@@ -191,7 +181,7 @@ public final class DeviceStateManagerFoldingFeatureProducer
*/
@NonNull
public List<CommonFoldingFeature> getFoldsWithUnknownState() {
- Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData();
+ final Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData();
if (optionalFoldingFeatureString.isPresent()) {
return CommonFoldingFeature.parseListFromString(
@@ -201,7 +191,6 @@ public final class DeviceStateManagerFoldingFeatureProducer
return Collections.emptyList();
}
-
/**
* Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the
* {@link DeviceStateManagerFoldingFeatureProducer}.
@@ -218,16 +207,16 @@ public final class DeviceStateManagerFoldingFeatureProducer
return foldFeatures;
}
-
/**
* Returns {@code true} if the device supports half-opened mode, {@code false} otherwise.
*/
public boolean isHalfOpenedSupported() {
- return mIsHalfOpenedSupported;
+ return mDeviceStateMapper.mIsHalfOpenedSupported;
}
/**
* Adds the data to the storeFeaturesConsumer when the data is ready.
+ *
* @param storeFeaturesConsumer a consumer to collect the data when it is first available.
*/
@Override
@@ -236,38 +225,123 @@ public final class DeviceStateManagerFoldingFeatureProducer
if (TextUtils.isEmpty(displayFeaturesString)) {
storeFeaturesConsumer.accept(new ArrayList<>());
} else {
- runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString);
+ final DeviceState state = getCurrentDeviceState();
+ runCallbackWhenValidState(state, storeFeaturesConsumer, displayFeaturesString);
}
});
}
- private void notifyFoldingFeatureChange(String displayFeaturesString) {
- if (!isCurrentStateValid()) {
+ @GuardedBy("mCurrentDeviceStateLock")
+ private void notifyFoldingFeatureChangeLocked(String displayFeaturesString) {
+ final DeviceState state = mCurrentDeviceState;
+ if (!mDeviceStateMapper.isDeviceStateValid(state)) {
return;
}
if (TextUtils.isEmpty(displayFeaturesString)) {
notifyDataChanged(new ArrayList<>());
} else {
- notifyDataChanged(calculateFoldingFeature(displayFeaturesString));
+ notifyDataChanged(calculateFoldingFeature(state, displayFeaturesString));
}
}
- private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) {
- return parseListFromString(displayFeaturesString, currentHingeState());
+ @NonNull
+ private List<CommonFoldingFeature> calculateFoldingFeature(@NonNull DeviceState deviceState,
+ @NonNull String displayFeaturesString) {
+ @CommonFoldingFeature.State
+ final int hingeState = mDeviceStateMapper.getHingeState(deviceState);
+ return parseListFromString(displayFeaturesString, hingeState);
}
- @CommonFoldingFeature.State
- private int currentHingeState() {
- @CommonFoldingFeature.State
- int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(),
- COMMON_STATE_UNKNOWN);
+ /**
+ * Internal class to map device states to corresponding postures.
+ *
+ * <p>This class encapsulates the logic for mapping device states to postures. The mapping is
+ * immutable after initialization to ensure thread safety.
+ */
+ private static class DeviceStateMapper {
+ /**
+ * Emulated device state
+ * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
+ * {@link CommonFoldingFeature.State} map.
+ *
+ * <p>This map must be immutable after initialization to ensure thread safety, as it may be
+ * accessed from multiple threads. Modifications should only occur during object
+ * construction.
+ */
+ private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
+
+ /**
+ * The list of device states that are supported.
+ *
+ * <p>This list must be immutable after initialization to ensure thread safety.
+ */
+ @NonNull
+ private final List<DeviceState> mSupportedStates;
+
+ final boolean mIsHalfOpenedSupported;
+
+ DeviceStateMapper(@NonNull Context context, @NonNull List<DeviceState> supportedStates) {
+ mSupportedStates = supportedStates;
+
+ final String[] deviceStatePosturePairs = context.getResources()
+ .getStringArray(R.array.config_device_state_postures);
+ boolean isHalfOpenedSupported = false;
+ for (String deviceStatePosturePair : deviceStatePosturePairs) {
+ final String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
+ if (deviceStatePostureMapping.length != 2) {
+ if (DEBUG) {
+ Log.e(TAG, "Malformed device state posture pair: "
+ + deviceStatePosturePair);
+ }
+ continue;
+ }
- if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
- posture = mDeviceStateToPostureMap.get(
- DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState,
- mSupportedStates), COMMON_STATE_UNKNOWN);
+ final int deviceState;
+ final int posture;
+ try {
+ deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
+ posture = Integer.parseInt(deviceStatePostureMapping[1]);
+ } catch (NumberFormatException e) {
+ if (DEBUG) {
+ Log.e(TAG, "Failed to parse device state or posture: "
+ + deviceStatePosturePair,
+ e);
+ }
+ continue;
+ }
+ isHalfOpenedSupported = isHalfOpenedSupported
+ || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
+ mDeviceStateToPostureMap.put(deviceState, posture);
+ }
+ mIsHalfOpenedSupported = isHalfOpenedSupported;
+ }
+
+ boolean isDeviceStateToPostureMapEmpty() {
+ return mDeviceStateToPostureMap.size() == 0;
+ }
+
+ /**
+ * Validates if the provided deviceState exists in the {@link #mDeviceStateToPostureMap}
+ * which was initialized in the constructor of {@link DeviceStateMapper}.
+ * Returns a boolean value of whether the device state is valid.
+ */
+ boolean isDeviceStateValid(@NonNull DeviceState deviceState) {
+ // If the device state is not found in the map, indexOfKey returns a negative number.
+ return mDeviceStateToPostureMap.indexOfKey(deviceState.getIdentifier()) >= 0;
}
- return posture;
+ @CommonFoldingFeature.State
+ int getHingeState(@NonNull DeviceState deviceState) {
+ @CommonFoldingFeature.State
+ final int posture =
+ mDeviceStateToPostureMap.get(deviceState.getIdentifier(), COMMON_STATE_UNKNOWN);
+ if (posture != CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
+ return posture;
+ }
+
+ final int baseStateIdentifier =
+ DeviceStateUtil.calculateBaseStateIdentifier(deviceState, mSupportedStates);
+ return mDeviceStateToPostureMap.get(baseStateIdentifier, COMMON_STATE_UNKNOWN);
+ }
}
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
index bd430c0e610b..09185ee203b8 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp
+++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
@@ -29,6 +29,7 @@ android_test {
srcs: [
"**/*.java",
+ "**/*.kt",
],
static_libs: [
@@ -41,6 +42,7 @@ android_test {
"androidx.test.ext.junit",
"flag-junit",
"mockito-target-extended-minus-junit4",
+ "mockito-kotlin-nodeps",
"truth",
"testables",
"platform-test-annotations",
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt
new file mode 100644
index 000000000000..90887a747a6f
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2024 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 androidx.window.common
+
+import android.content.Context
+import android.content.res.Resources
+import android.hardware.devicestate.DeviceState
+import android.hardware.devicestate.DeviceStateManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.window.common.layout.CommonFoldingFeature
+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_NO_FOLDING_FEATURES
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE
+import androidx.window.common.layout.DisplayFoldFeatureCommon
+import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED
+import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN
+import com.android.internal.R
+import com.android.window.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+
+/**
+ * Test class for [DeviceStateManagerFoldingFeatureProducer].
+ *
+ * Build/Install/Run:
+ * atest WMJetpackUnitTests:DeviceStateManagerFoldingFeatureProducerTest
+ */
+@RunWith(AndroidJUnit4::class)
+class DeviceStateManagerFoldingFeatureProducerTest {
+ @get:Rule
+ val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+ private val mMockDeviceStateManager = mock<DeviceStateManager>()
+ private val mMockResources = mock<Resources> {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn DEVICE_STATE_POSTURES
+ }
+ private val mMockContext = mock<Context> {
+ on { resources } doReturn mMockResources
+ }
+ private val mRawFoldSupplier = mock<RawFoldingFeatureProducer> {
+ on { currentData } doReturn Optional.of(DISPLAY_FEATURES)
+ on { getData(any<Consumer<String>>()) } doAnswer { invocation ->
+ val callback = invocation.getArgument(0) as Consumer<String>
+ callback.accept(DISPLAY_FEATURES)
+ }
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_WLINFO_ONCREATE)
+ fun testRegisterCallback_whenWlinfoOncreateIsDisabled_usesMainExecutor() {
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager).registerCallback(eq(mMockContext.mainExecutor), any())
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_WLINFO_ONCREATE)
+ fun testRegisterCallback_whenWlinfoOncreateIsEnabled_usesRunnableRun() {
+ val executorCaptor = ArgumentCaptor.forClass(Executor::class.java)
+ val runnable = mock<Runnable>()
+
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager).registerCallback(executorCaptor.capture(), any())
+ executorCaptor.value.execute(runnable)
+ verify(runnable).run()
+ }
+
+ @Test
+ fun testGetCurrentData_validCurrentState_returnsFoldingFeatureWithState() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+
+ val currentData = ffp.getCurrentData()
+
+ assertThat(currentData).isPresent()
+ assertThat(currentData.get()).containsExactlyElementsIn(HALF_OPENED_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testGetCurrentData_invalidCurrentState_returnsEmptyOptionalFoldingFeature() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val currentData = ffp.getCurrentData()
+
+ assertThat(currentData).isEmpty()
+ }
+
+ @Test
+ fun testGetFoldsWithUnknownState_validFoldingFeature_returnsFoldingFeaturesWithUnknownState() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val result = ffp.getFoldsWithUnknownState()
+
+ assertThat(result).containsExactlyElementsIn(UNKNOWN_STATE_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testGetFoldsWithUnknownState_emptyFoldingFeature_returnsEmptyList() {
+ mRawFoldSupplier.stub {
+ on { currentData } doReturn Optional.empty()
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val result = ffp.getFoldsWithUnknownState()
+
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun testGetDisplayFeatures_validFoldingFeature_returnsDisplayFoldFeatures() {
+ mRawFoldSupplier.stub {
+ on { currentData } doReturn Optional.of(DISPLAY_FEATURES_HALF_OPENED_HINGE)
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val result = ffp.displayFeatures
+
+ assertThat(result).containsExactly(
+ DisplayFoldFeatureCommon(
+ DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN,
+ setOf(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED),
+ ),
+ )
+ }
+
+ @Test
+ fun testIsHalfOpenedSupported_withHalfOpenedPostures_returnsTrue() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ assertThat(ffp.isHalfOpenedSupported).isTrue()
+ }
+
+ @Test
+ fun testIsHalfOpenedSupported_withEmptyPostures_returnsFalse() {
+ mMockResources.stub {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn emptyArray()
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ assertThat(ffp.isHalfOpenedSupported).isFalse()
+ }
+
+ @Test
+ fun testGetData_emptyDisplayFeaturesString_callsConsumerWithEmptyList() {
+ mRawFoldSupplier.stub {
+ on { getData(any<Consumer<String>>()) } doAnswer { invocation ->
+ val callback = invocation.getArgument(0) as Consumer<String>
+ callback.accept("")
+ }
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+ ffp.getData(storeFeaturesConsumer)
+
+ verify(storeFeaturesConsumer).accept(emptyList())
+ }
+
+ @Test
+ fun testGetData_validState_callsConsumerWithFoldingFeatures() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+ val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+ ffp.getData(storeFeaturesConsumer)
+
+ verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testGetData_invalidState_addsAcceptOnceConsumerToDataChangedCallback() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+ ffp.getData(storeFeaturesConsumer)
+
+ verify(storeFeaturesConsumer, never()).accept(any())
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_OPENED)
+ verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testDeviceStateMapper_malformedDeviceStatePosturePair_skipsPair() {
+ val malformedDeviceStatePostures = arrayOf(
+ // Missing the posture.
+ "0",
+ // Empty string.
+ "",
+ // Too many elements.
+ "0:1:2",
+ )
+ mMockResources.stub {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn
+ malformedDeviceStatePostures
+ }
+
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager, never()).registerCallback(any(), any())
+ }
+
+ @Test
+ fun testDeviceStateMapper_invalidNumberFormat_skipsPair() {
+ val invalidNumberFormatDeviceStatePostures = arrayOf("a:1", "0:b", "a:b", ":1")
+ mMockResources.stub {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn
+ invalidNumberFormatDeviceStatePostures
+ }
+
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager, never()).registerCallback(any(), any())
+ }
+
+ companion object {
+ // Supported device states configuration.
+ private enum class SupportedDeviceStates {
+ CLOSED, HALF_OPENED, OPENED, REAR_DISPLAY, CONCURRENT;
+
+ override fun toString() = ordinal.toString()
+
+ fun toDeviceState(): DeviceState =
+ DeviceState(DeviceState.Configuration.Builder(ordinal, name).build())
+ }
+
+ // Map of supported device states supplied by DeviceStateManager to WM Jetpack posture.
+ private val DEVICE_STATE_POSTURES =
+ arrayOf(
+ "${SupportedDeviceStates.CLOSED}:$COMMON_STATE_NO_FOLDING_FEATURES",
+ "${SupportedDeviceStates.HALF_OPENED}:$COMMON_STATE_HALF_OPENED",
+ "${SupportedDeviceStates.OPENED}:$COMMON_STATE_FLAT",
+ "${SupportedDeviceStates.REAR_DISPLAY}:$COMMON_STATE_NO_FOLDING_FEATURES",
+ "${SupportedDeviceStates.CONCURRENT}:$COMMON_STATE_USE_BASE_STATE",
+ )
+ private val DEVICE_STATE_HALF_OPENED = SupportedDeviceStates.HALF_OPENED.toDeviceState()
+ private val DEVICE_STATE_OPENED = SupportedDeviceStates.OPENED.toDeviceState()
+
+ // WindowsManager Jetpack display features.
+ private val DISPLAY_FEATURES = "fold-[1104,0,1104,1848]"
+ private val DISPLAY_FEATURES_HALF_OPENED_HINGE = "$DISPLAY_FEATURES-half-opened"
+ private val HALF_OPENED_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString(
+ DISPLAY_FEATURES,
+ COMMON_STATE_HALF_OPENED,
+ )
+ private val UNKNOWN_STATE_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString(
+ DISPLAY_FEATURES,
+ COMMON_STATE_UNKNOWN,
+ )
+ }
+}