Loading libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +172 −98 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; /** Loading @@ -54,13 +60,6 @@ public final class DeviceStateManagerFoldingFeatureProducer DeviceStateManagerFoldingFeatureProducer.class.getSimpleName(); 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)}. Loading @@ -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) { synchronized (mCurrentDeviceStateLock) { mCurrentDeviceState = state; mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer .this::notifyFoldingFeatureChange); mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this ::notifyFoldingFeatureChangeLocked); } } }; Loading @@ -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); } } Loading @@ -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(); synchronized (mCurrentDeviceStateLock) { if (hasListeners()) { mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange); mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChangeLocked); } else { mCurrentDeviceState = new DeviceState( new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, "INVALID").build()); mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange); 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())); } } Loading @@ -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( Loading @@ -201,7 +191,6 @@ public final class DeviceStateManagerFoldingFeatureProducer return Collections.emptyList(); } /** * Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the * {@link DeviceStateManagerFoldingFeatureProducer}. Loading @@ -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 Loading @@ -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; if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { posture = mDeviceStateToPostureMap.get( DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState, mSupportedStates), COMMON_STATE_UNKNOWN); 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; } 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; } @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); } } } libs/WindowManager/Jetpack/tests/unittest/Android.bp +2 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ android_test { srcs: [ "**/*.java", "**/*.kt", ], static_libs: [ Loading @@ -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", Loading libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt 0 → 100644 +341 −0 File added.Preview size limit exceeded, changes collapsed. Show changes Loading
libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +172 −98 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; /** Loading @@ -54,13 +60,6 @@ public final class DeviceStateManagerFoldingFeatureProducer DeviceStateManagerFoldingFeatureProducer.class.getSimpleName(); 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)}. Loading @@ -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) { synchronized (mCurrentDeviceStateLock) { mCurrentDeviceState = state; mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer .this::notifyFoldingFeatureChange); mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this ::notifyFoldingFeatureChangeLocked); } } }; Loading @@ -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); } } Loading @@ -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(); synchronized (mCurrentDeviceStateLock) { if (hasListeners()) { mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange); mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChangeLocked); } else { mCurrentDeviceState = new DeviceState( new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, "INVALID").build()); mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange); 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())); } } Loading @@ -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( Loading @@ -201,7 +191,6 @@ public final class DeviceStateManagerFoldingFeatureProducer return Collections.emptyList(); } /** * Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the * {@link DeviceStateManagerFoldingFeatureProducer}. Loading @@ -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 Loading @@ -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; if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { posture = mDeviceStateToPostureMap.get( DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState, mSupportedStates), COMMON_STATE_UNKNOWN); 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; } 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; } @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); } } }
libs/WindowManager/Jetpack/tests/unittest/Android.bp +2 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ android_test { srcs: [ "**/*.java", "**/*.kt", ], static_libs: [ Loading @@ -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", Loading
libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt 0 → 100644 +341 −0 File added.Preview size limit exceeded, changes collapsed. Show changes