diff options
| author | 2024-01-15 14:27:52 +0000 | |
|---|---|---|
| committer | 2024-01-15 14:27:52 +0000 | |
| commit | d73646d44a523e5457afe08c8965c91a8dc2c191 (patch) | |
| tree | 7c8afedcf2e8ea51602e820a5e0becce5e7ea938 | |
| parent | 2713b3b1a61d72658d10627c8d0692664d474720 (diff) | |
| parent | ceed6f0b281029fc07f63fd3c230a9603abf421d (diff) | |
Merge "Add foldables posture based closed device state" into main
| -rw-r--r-- | services/foldables/devicestateprovider/proguard.flags | 2 | ||||
| -rw-r--r-- | services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java | 432 | ||||
| -rw-r--r-- | services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java (renamed from services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java) | 62 | ||||
| -rw-r--r-- | services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java | 309 | ||||
| -rw-r--r-- | services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java | 722 | ||||
| -rw-r--r-- | services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java | 703 | ||||
| -rw-r--r-- | services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java | 81 |
7 files changed, 2292 insertions, 19 deletions
diff --git a/services/foldables/devicestateprovider/proguard.flags b/services/foldables/devicestateprovider/proguard.flags index 069cbc642050..b810cad5217d 100644 --- a/services/foldables/devicestateprovider/proguard.flags +++ b/services/foldables/devicestateprovider/proguard.flags @@ -1 +1 @@ --keep,allowoptimization,allowaccessmodification class com.android.server.policy.TentModeDeviceStatePolicy { *; } +-keep,allowoptimization,allowaccessmodification class com.android.server.policy.BookStyleDeviceStatePolicy { *; } diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java new file mode 100644 index 000000000000..d5a3cffd71dd --- /dev/null +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java @@ -0,0 +1,432 @@ +/* + * 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.server.policy; + +import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen.OUTER; +import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_0; +import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_0_TO_45; +import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_45_TO_90; +import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_90_TO_180; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.display.DisplayManager; +import android.os.Handler; +import android.util.ArraySet; +import android.view.Display; +import android.view.Surface; + +import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen; +import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle; +import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition; +import com.android.server.policy.BookStyleClosedStatePredicate.ConditionSensorListener.SensorSubscription; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * 'Closed' state predicate that takes into account the posture of the device + * It accepts list of state transitions that control how the device moves between + * device states. + * See {@link BookStyleStateTransitions} for detailed description of the default behavior. + */ +public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceStateProvider>, + DisplayManager.DisplayListener { + + private final BookStylePreferredScreenCalculator mClosedStateCalculator; + private final Handler mHandler = new Handler(); + private final PostureEstimator mPostureEstimator; + private final DisplayManager mDisplayManager; + + /** + * Creates {@link BookStyleClosedStatePredicate}. It is expected that the device has a pair + * of accelerometer sensors (one for each movable part of the device), see parameter + * descriptions for the behaviour when these sensors are not available. + * @param context context that could be used to get system services + * @param updatesListener callback that will be executed whenever the predicate should be + * checked again + * @param leftAccelerometerSensor accelerometer sensor that is located in the half of the + * device that has the outer screen, in case if this sensor is + * not provided, tent/wedge mode will be detected only using + * orientation sensor and screen rotation, so this mode won't + * be accessible by putting the device on a flat surface + * @param rightAccelerometerSensor accelerometer sensor that is located on the opposite side + * across the hinge from the previous accelerometer sensor, + * in case if this sensor is not provided, reverse wedge mode + * won't be detected, so the device will use closed state using + * constant angle when folding + * @param stateTransitions definition of all possible state transitions, see + * {@link BookStyleStateTransitions} for sample and more details + */ + + public BookStyleClosedStatePredicate(@NonNull Context context, + @NonNull ClosedStateUpdatesListener updatesListener, + @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor, + @NonNull List<StateTransition> stateTransitions) { + mDisplayManager = context.getSystemService(DisplayManager.class); + mDisplayManager.registerDisplayListener(this, mHandler); + + mClosedStateCalculator = new BookStylePreferredScreenCalculator(stateTransitions); + + final SensorManager sensorManager = context.getSystemService(SensorManager.class); + final Sensor orientationSensor = sensorManager.getDefaultSensor( + Sensor.TYPE_DEVICE_ORIENTATION); + + mPostureEstimator = new PostureEstimator(mHandler, sensorManager, + leftAccelerometerSensor, rightAccelerometerSensor, orientationSensor, + updatesListener::onClosedStateUpdated); + } + + /** + * Based on the current sensor readings and current state, returns true if the device should use + * 'CLOSED' device state and false if it should not use 'CLOSED' state (e.g. could use half-open + * or open states). + */ + @Override + public boolean test(FoldableDeviceStateProvider foldableDeviceStateProvider) { + final HingeAngle hingeAngle = hingeAngleFromFloat( + foldableDeviceStateProvider.getHingeAngle()); + + mPostureEstimator.onDeviceClosedStatusChanged(hingeAngle == ANGLE_0); + + final PreferredScreen preferredScreen = mClosedStateCalculator. + calculatePreferredScreen(hingeAngle, mPostureEstimator.isLikelyTentOrWedgeMode(), + mPostureEstimator.isLikelyReverseWedgeMode(hingeAngle)); + + return preferredScreen == OUTER; + } + + private HingeAngle hingeAngleFromFloat(float hingeAngle) { + if (hingeAngle == 0f) { + return ANGLE_0; + } else if (hingeAngle < 45f) { + return ANGLE_0_TO_45; + } else if (hingeAngle < 90f) { + return ANGLE_45_TO_90; + } else { + return ANGLE_90_TO_180; + } + } + + @Override + public void onDisplayChanged(int displayId) { + if (displayId == DEFAULT_DISPLAY) { + final Display display = mDisplayManager.getDisplay(displayId); + int displayState = display.getState(); + boolean isDisplayOn = displayState == Display.STATE_ON; + mPostureEstimator.onDisplayPowerStatusChanged(isDisplayOn); + mPostureEstimator.onDisplayRotationChanged(display.getRotation()); + } + } + + @Override + public void onDisplayAdded(int displayId) { + + } + + @Override + public void onDisplayRemoved(int displayId) { + + } + + public interface ClosedStateUpdatesListener { + void onClosedStateUpdated(); + } + + /** + * Estimates if the device is going to enter wedge/tent mode based on the sensor data + */ + private static class PostureEstimator implements SensorEventListener { + + + private static final int FLAT_INCLINATION_THRESHOLD_DEGREES = 8; + + /** + * Alpha parameter of the accelerometer low pass filter: the lower the value, the less high + * frequency noise it filter but reduces the latency. + */ + private static final float GRAVITY_VECTOR_LOW_PASS_ALPHA_VALUE = 0.8f; + + + @Nullable + private final Sensor mLeftAccelerometerSensor; + @Nullable + private final Sensor mRightAccelerometerSensor; + private final Sensor mOrientationSensor; + private final Runnable mOnSensorUpdatedListener; + + private final ConditionSensorListener mConditionedSensorListener; + + @Nullable + private float[] mRightGravityVector; + + @Nullable + private float[] mLeftGravityVector; + + @Nullable + private Integer mLastScreenRotation; + + @Nullable + private SensorEvent mLastDeviceOrientationSensorEvent = null; + + private boolean mScreenTurnedOn = false; + private boolean mDeviceClosed = false; + + public PostureEstimator(Handler handler, SensorManager sensorManager, + @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor, + Sensor orientationSensor, Runnable onSensorUpdated) { + mLeftAccelerometerSensor = leftAccelerometerSensor; + mRightAccelerometerSensor = rightAccelerometerSensor; + mOrientationSensor = orientationSensor; + + mOnSensorUpdatedListener = onSensorUpdated; + + final List<SensorSubscription> sensorSubscriptions = new ArrayList<>(); + if (mLeftAccelerometerSensor != null) { + sensorSubscriptions.add(new SensorSubscription( + mLeftAccelerometerSensor, + /* allowedToListen= */ () -> mScreenTurnedOn && !mDeviceClosed, + /* cleanup= */ () -> mLeftGravityVector = null)); + } + + if (mRightAccelerometerSensor != null) { + sensorSubscriptions.add(new SensorSubscription( + mRightAccelerometerSensor, + /* allowedToListen= */ () -> mScreenTurnedOn, + /* cleanup= */ () -> mRightGravityVector = null)); + } + + sensorSubscriptions.add(new SensorSubscription(mOrientationSensor, + /* allowedToListen= */ () -> mScreenTurnedOn, + /* cleanup= */ () -> mLastDeviceOrientationSensorEvent = null)); + + mConditionedSensorListener = new ConditionSensorListener(sensorManager, this, handler, + sensorSubscriptions); + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor == mRightAccelerometerSensor) { + if (mRightGravityVector == null) { + mRightGravityVector = new float[3]; + } + setNewValueWithHighPassFilter(mRightGravityVector, event.values); + + final boolean isRightMostlyFlat = Objects.equals( + isGravityVectorMostlyFlat(mRightGravityVector), Boolean.TRUE); + + if (isRightMostlyFlat) { + // Reset orientation sensor when the device becomes flat + mLastDeviceOrientationSensorEvent = null; + } + } else if (event.sensor == mLeftAccelerometerSensor) { + if (mLeftGravityVector == null) { + mLeftGravityVector = new float[3]; + } + setNewValueWithHighPassFilter(mLeftGravityVector, event.values); + } else if (event.sensor == mOrientationSensor) { + mLastDeviceOrientationSensorEvent = event; + } + + mOnSensorUpdatedListener.run(); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + + } + + private void setNewValueWithHighPassFilter(float[] output, float[] newValues) { + final float alpha = GRAVITY_VECTOR_LOW_PASS_ALPHA_VALUE; + output[0] = alpha * output[0] + (1 - alpha) * newValues[0]; + output[1] = alpha * output[1] + (1 - alpha) * newValues[1]; + output[2] = alpha * output[2] + (1 - alpha) * newValues[2]; + } + + /** + * Returns true if the phone likely in reverse wedge mode (when a foldable phone is lying + * on the outer screen mostly flat to the ground) + */ + public boolean isLikelyReverseWedgeMode(HingeAngle hingeAngle) { + return hingeAngle != ANGLE_0 && Objects.equals( + isGravityVectorMostlyFlat(mLeftGravityVector), Boolean.TRUE); + } + + /** + * Returns true if the phone is likely in tent or wedge mode when unfolding. Tent mode + * is detected by checking if the phone is in seascape position, screen is rotated to + * landscape or seascape, or if the right side of the device is mostly flat. + */ + public boolean isLikelyTentOrWedgeMode() { + boolean isScreenLandscapeOrSeascape = Objects.equals(mLastScreenRotation, + Surface.ROTATION_270) || Objects.equals(mLastScreenRotation, + Surface.ROTATION_90); + if (isScreenLandscapeOrSeascape) { + return true; + } + + boolean isRightMostlyFlat = Objects.equals( + isGravityVectorMostlyFlat(mRightGravityVector), Boolean.TRUE); + if (isRightMostlyFlat) { + return true; + } + + boolean isSensorSeaScape = Objects.equals(getOrientationSensorRotation(), + Surface.ROTATION_270); + if (isSensorSeaScape) { + return true; + } + + return false; + } + + /** + * Returns true if the passed gravity vector implies that the phone is mostly flat (the + * vector is close to be perpendicular to the ground and has a positive Z component). + * Returns null if there is no data from the sensor. + */ + private Boolean isGravityVectorMostlyFlat(@Nullable float[] vector) { + if (vector == null) return null; + if (vector[0] == 0.0f && vector[1] == 0.0f && vector[2] == 0.0f) { + // Likely we haven't received the actual data yet, treat it as no data + return null; + } + + double vectorMagnitude = Math.sqrt( + vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]); + float normalizedGravityZ = (float) (vector[2] / vectorMagnitude); + + final int inclination = (int) Math.round(Math.toDegrees(Math.acos(normalizedGravityZ))); + return inclination < FLAT_INCLINATION_THRESHOLD_DEGREES; + } + + private Integer getOrientationSensorRotation() { + if (mLastDeviceOrientationSensorEvent == null) return null; + return (int) mLastDeviceOrientationSensorEvent.values[0]; + } + + /** + * Called whenever display status changes, we use this signal to start/stop listening + * to sensors when the display is off to save battery. Using display state instead of + * general power state to reduce the time when sensors are on, we don't need to listen + * to the extra sensors when the screen is off. + */ + public void onDisplayPowerStatusChanged(boolean screenTurnedOn) { + mScreenTurnedOn = screenTurnedOn; + mConditionedSensorListener.updateListeningState(); + } + + /** + * Called whenever we display rotation might have been updated + * @param rotation new rotation + */ + public void onDisplayRotationChanged(int rotation) { + mLastScreenRotation = rotation; + } + + /** + * Called whenever foldable device becomes fully closed or opened + */ + public void onDeviceClosedStatusChanged(boolean deviceClosed) { + mDeviceClosed = deviceClosed; + mConditionedSensorListener.updateListeningState(); + } + } + + /** + * Helper class that subscribes or unsubscribes from a sensor based on a condition specified + * in {@link SensorSubscription} + */ + static class ConditionSensorListener { + private final List<SensorSubscription> mSensorSubscriptions; + private final ArraySet<Sensor> mIsListening = new ArraySet<>(); + + private final SensorManager mSensorManager; + private final SensorEventListener mSensorEventListener; + + private final Handler mHandler; + + public ConditionSensorListener(SensorManager sensorManager, + SensorEventListener sensorEventListener, Handler handler, + List<SensorSubscription> sensorSubscriptions) { + mSensorManager = sensorManager; + mSensorEventListener = sensorEventListener; + mSensorSubscriptions = sensorSubscriptions; + mHandler = handler; + } + + /** + * Updates current listening state of the sensor based on the provided conditions + */ + public void updateListeningState() { + for (int i = 0; i < mSensorSubscriptions.size(); i++) { + final SensorSubscription subscription = mSensorSubscriptions.get(i); + final Sensor sensor = subscription.mSensor; + + final boolean shouldBeListening = subscription.mAllowedToListenSupplier.get(); + final boolean isListening = mIsListening.contains(sensor); + final boolean shouldUpdateListening = isListening != shouldBeListening; + + if (shouldUpdateListening) { + if (shouldBeListening) { + mIsListening.add(sensor); + mSensorManager.registerListener(mSensorEventListener, sensor, + SENSOR_DELAY_NORMAL, mHandler); + } else { + mIsListening.remove(sensor); + mSensorManager.unregisterListener(mSensorEventListener, sensor); + subscription.mOnUnsubscribe.run(); + } + } + } + } + + /** + * Represents a configuration of a single sensor subscription + */ + public static class SensorSubscription { + private final Sensor mSensor; + private final Supplier<Boolean> mAllowedToListenSupplier; + private final Runnable mOnUnsubscribe; + + /** + * @param sensor sensor to listen to + * @param allowedToListen return true when it is allowed to listen to the sensor + * @param cleanup a runnable that will be closed just before unsubscribing from the + * sensor + */ + + public SensorSubscription(Sensor sensor, Supplier<Boolean> allowedToListen, + Runnable cleanup) { + mSensor = sensor; + mAllowedToListenSupplier = allowedToListen; + mOnUnsubscribe = cleanup; + } + } + } +} diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java index 5968b6346d35..ad938aff396a 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java @@ -21,10 +21,12 @@ import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_WHEN_REQUES import static com.android.server.devicestate.DeviceState.FLAG_EMULATED_ONLY; import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE; import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL; +import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS; import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig; import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createTentModeClosedState; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorManager; @@ -39,12 +41,15 @@ import com.android.server.policy.feature.flags.FeatureFlagsImpl; import java.util.function.Predicate; /** - * Device state policy for a foldable device that supports tent mode: a mode when the device - * keeps the outer display on until reaching a certain hinge angle threshold. + * Device state policy for a foldable device with two screens in a book style, where the hinge is + * located on the left side of the device when in folded posture. + * The policy supports tent/wedge mode: a mode when the device keeps the outer display on + * until reaching certain conditions like hinge angle threshold. * * Contains configuration for {@link FoldableDeviceStateProvider}. */ -public class TentModeDeviceStatePolicy extends DeviceStatePolicy { +public class BookStyleDeviceStatePolicy extends DeviceStatePolicy implements + BookStyleClosedStatePredicate.ClosedStateUpdatesListener { private static final int DEVICE_STATE_CLOSED = 0; private static final int DEVICE_STATE_HALF_OPENED = 1; @@ -57,9 +62,10 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { private static final int MIN_CLOSED_ANGLE_DEGREES = 0; private static final int MAX_CLOSED_ANGLE_DEGREES = 5; - private final DeviceStateProvider mProvider; + private final FoldableDeviceStateProvider mProvider; private final boolean mIsDualDisplayBlockingEnabled; + private final boolean mEnablePostureBasedClosedState; private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true; private static final Predicate<FoldableDeviceStateProvider> NOT_ALLOWED = p -> false; @@ -73,30 +79,30 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { * between folded and unfolded modes, otherwise when folding the * display switch will happen at 0 degrees */ - public TentModeDeviceStatePolicy(@NonNull Context context, - @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, int closeAngleDegrees) { - this(new FeatureFlagsImpl(), context, hingeAngleSensor, hallSensor, closeAngleDegrees); - } - - public TentModeDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context, - @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, - int closeAngleDegrees) { + public BookStyleDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context, + @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, + @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor, + Integer closeAngleDegrees) { super(context); final SensorManager sensorManager = mContext.getSystemService(SensorManager.class); final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); - final DeviceStateConfiguration[] configuration = createConfiguration(closeAngleDegrees); - + mEnablePostureBasedClosedState = featureFlags.enableFoldablesPostureBasedClosedState(); mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking(); + final DeviceStateConfiguration[] configuration = createConfiguration( + leftAccelerometerSensor, rightAccelerometerSensor, closeAngleDegrees); + mProvider = new FoldableDeviceStateProvider(mContext, sensorManager, hingeAngleSensor, hallSensor, displayManager, configuration); } - private DeviceStateConfiguration[] createConfiguration(int closeAngleDegrees) { + private DeviceStateConfiguration[] createConfiguration(@Nullable Sensor leftAccelerometerSensor, + @Nullable Sensor rightAccelerometerSensor, Integer closeAngleDegrees) { return new DeviceStateConfiguration[]{ - createClosedConfiguration(closeAngleDegrees), + createClosedConfiguration(leftAccelerometerSensor, rightAccelerometerSensor, + closeAngleDegrees), createConfig(DEVICE_STATE_HALF_OPENED, /* name= */ "HALF_OPENED", /* activeStatePredicate= */ (provider) -> { @@ -123,8 +129,10 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { }; } - private DeviceStateConfiguration createClosedConfiguration(int closeAngleDegrees) { - if (closeAngleDegrees > 0) { + private DeviceStateConfiguration createClosedConfiguration( + @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor, + @Nullable Integer closeAngleDegrees) { + if (closeAngleDegrees != null) { // Switch displays at closeAngleDegrees in both ways (folding and unfolding) return createConfig( DEVICE_STATE_CLOSED, @@ -137,6 +145,19 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { ); } + if (mEnablePostureBasedClosedState) { + // Use smart closed state predicate that will use different switch angles + // based on the device posture (e.g. wedge mode, tent mode, reverse wedge mode) + return createConfig( + DEVICE_STATE_CLOSED, + /* name= */ "CLOSED", + /* flags= */ FLAG_CANCEL_OVERRIDE_REQUESTS, + /* activeStatePredicate= */ new BookStyleClosedStatePredicate(mContext, + this, leftAccelerometerSensor, rightAccelerometerSensor, + DEFAULT_STATE_TRANSITIONS) + ); + } + // Switch to the outer display only at 0 degrees but use TENT_MODE_SWITCH_ANGLE_DEGREES // angle when switching to the inner display return createTentModeClosedState(DEVICE_STATE_CLOSED, @@ -148,6 +169,11 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { } @Override + public void onClosedStateUpdated() { + mProvider.notifyDeviceStateChangedIfNeeded(); + } + + @Override public DeviceStateProvider getDeviceStateProvider() { return mProvider; } diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java new file mode 100644 index 000000000000..8977422a90a8 --- /dev/null +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java @@ -0,0 +1,309 @@ +/* + * 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.server.policy; + +import android.annotation.Nullable; + +import java.util.List; +import java.util.Objects; + +/** + * Calculates if we should use outer or inner display on foldable devices based on a several + * inputs like device orientation, hinge angle signals. + * + * This is a stateful class and acts like a state machine with fixed number of states + * and transitions. It allows to list all possible state transitions instead of performing + * imperative logic to make sure that we cover all scenarios and improve debuggability. + * + * See {@link BookStyleStateTransitions} for detailed description of the default behavior. + */ +public class BookStylePreferredScreenCalculator { + + /** + * When calculating the new state we will re-calculate it until it settles down. We re-calculate + * it because the new state might trigger another state transition and this might happen + * several times. We don't want to have infinite loops in state calculation, so this value + * limits the number of such state transitions. + * For example, in the default configuration {@link BookStyleStateTransitions}, after each + * transition with 'set sticky flag' output it will perform a transition to a state without + * 'set sticky flag' output. + * We also have a unit test covering all possible states which checks that we don't have such + * states that could end up in an infinite transition. See sample test for the default + * transitions in {@link BookStyleClosedStateCalculatorTest}. + */ + private static final int MAX_STATE_CHANGES = 16; + + private State mState = new State( + /* stickyKeepOuterUntil90Degrees= */ false, + /* stickyKeepInnerUntil45Degrees= */ false, + PreferredScreen.INVALID); + + private final List<StateTransition> mStateTransitions; + + /** + * Creates BookStyleClosedStateCalculator + * @param stateTransitions list of all state transitions + */ + public BookStylePreferredScreenCalculator(List<StateTransition> stateTransitions) { + mStateTransitions = stateTransitions; + } + + /** + * Calculates updated {@link PreferredScreen} based on the current inputs and the current state. + * The calculation is done based on defined {@link StateTransition}s, it might perform + * multiple transitions until we settle down on a single state. Multiple transitions could be + * performed in case if {@link StateTransition} causes another update of the state. + * There is a limit of maximum {@link MAX_STATE_CHANGES} state transitions, after which + * this method will throw an {@link IllegalStateException}. + * + * @param angle current hinge angle + * @param likelyTentOrWedge true if the device is likely in tent or wedge mode + * @param likelyReverseWedge true if the device is likely in reverse wedge mode + * @return updated {@link PreferredScreen} + */ + public PreferredScreen calculatePreferredScreen(HingeAngle angle, boolean likelyTentOrWedge, + boolean likelyReverseWedge) { + int attempts = 0; + State newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge); + while (attempts < MAX_STATE_CHANGES && !Objects.equals(mState, newState)) { + mState = newState; + newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge); + attempts++; + } + + if (attempts >= MAX_STATE_CHANGES) { + throw new IllegalStateException( + "Can't settle state " + mState + ", inputs: hingeAngle = " + angle + + ", likelyTentOrWedge = " + likelyTentOrWedge + + ", likelyReverseWedge = " + likelyReverseWedge); + } + + final State oldState = mState; + mState = newState; + + if (mState.mPreferredScreen == PreferredScreen.INVALID) { + throw new IllegalStateException( + "Reached invalid state " + mState + ", inputs: hingeAngle = " + angle + + ", likelyTentOrWedge = " + likelyTentOrWedge + + ", likelyReverseWedge = " + likelyReverseWedge + ", old state: " + + oldState); + } + + return mState.mPreferredScreen; + } + + /** + * Returns the current state of the calculator + */ + public State getState() { + return mState; + } + + private State calculateNewState(State current, HingeAngle hingeAngle, boolean likelyTentOrWedge, + boolean likelyReverseWedge) { + for (int i = 0; i < mStateTransitions.size(); i++) { + final State newState = mStateTransitions.get(i).tryTransition(hingeAngle, + likelyTentOrWedge, likelyReverseWedge, current); + if (newState != null) { + return newState; + } + } + + throw new IllegalArgumentException( + "Entry not found for state: " + current + ", hingeAngle = " + hingeAngle + + ", likelyTentOrWedge = " + likelyTentOrWedge + ", likelyReverseWedge = " + + likelyReverseWedge); + } + + /** + * The angle between two halves of the foldable device in degrees. The angle is '0' when + * the device is fully closed and '180' when the device is fully open and flat. + */ + public enum HingeAngle { + ANGLE_0, + ANGLE_0_TO_45, + ANGLE_45_TO_90, + ANGLE_90_TO_180 + } + + /** + * Resulting closed state of the device, where OPEN state indicates that the device should use + * the inner display and CLOSED means that it should use the outer (cover) screen. + */ + public enum PreferredScreen { + INNER, + OUTER, + INVALID + } + + /** + * Describes a state transition for the posture based active screen calculator + */ + public static class StateTransition { + private final Input mInput; + private final State mOutput; + + public StateTransition(HingeAngle hingeAngle, boolean likelyTentOrWedge, + boolean likelyReverseWedge, + boolean stickyKeepOuterUntil90Degrees, boolean stickyKeepInnerUntil45Degrees, + PreferredScreen preferredScreen, Boolean setStickyKeepOuterUntil90Degrees, + Boolean setStickyKeepInnerUntil45Degrees) { + mInput = new Input(hingeAngle, likelyTentOrWedge, likelyReverseWedge, + stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees); + mOutput = new State(setStickyKeepOuterUntil90Degrees, + setStickyKeepInnerUntil45Degrees, preferredScreen); + } + + /** + * Returns true if the state transition is applicable for the given inputs + */ + private boolean isApplicable(HingeAngle hingeAngle, boolean likelyTentOrWedge, + boolean likelyReverseWedge, State currentState) { + return mInput.hingeAngle == hingeAngle + && mInput.likelyTentOrWedge == likelyTentOrWedge + && mInput.likelyReverseWedge == likelyReverseWedge + && Objects.equals(mInput.stickyKeepOuterUntil90Degrees, + currentState.stickyKeepOuterUntil90Degrees) + && Objects.equals(mInput.stickyKeepInnerUntil45Degrees, + currentState.stickyKeepInnerUntil45Degrees); + } + + /** + * Try to perform transition for the inputs, returns new state if this + * transition is applicable for the given state and inputs + */ + @Nullable + State tryTransition(HingeAngle hingeAngle, boolean likelyTentOrWedge, + boolean likelyReverseWedge, State currentState) { + if (!isApplicable(hingeAngle, likelyTentOrWedge, likelyReverseWedge, currentState)) { + return null; + } + + boolean stickyKeepOuterUntil90Degrees = currentState.stickyKeepOuterUntil90Degrees; + boolean stickyKeepInnerUntil45Degrees = currentState.stickyKeepInnerUntil45Degrees; + + if (mOutput.stickyKeepOuterUntil90Degrees != null) { + stickyKeepOuterUntil90Degrees = + mOutput.stickyKeepOuterUntil90Degrees; + } + + if (mOutput.stickyKeepInnerUntil45Degrees != null) { + stickyKeepInnerUntil45Degrees = + mOutput.stickyKeepInnerUntil45Degrees; + } + + return new State(stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees, + mOutput.mPreferredScreen); + } + } + + /** + * The input part of the {@link StateTransition}, these are the values that are used + * to decide which {@link State} output to choose. + */ + private static class Input { + final HingeAngle hingeAngle; + final boolean likelyTentOrWedge; + final boolean likelyReverseWedge; + final boolean stickyKeepOuterUntil90Degrees; + final boolean stickyKeepInnerUntil45Degrees; + + public Input(HingeAngle hingeAngle, boolean likelyTentOrWedge, + boolean likelyReverseWedge, + boolean stickyKeepOuterUntil90Degrees, boolean stickyKeepInnerUntil45Degrees) { + this.hingeAngle = hingeAngle; + this.likelyTentOrWedge = likelyTentOrWedge; + this.likelyReverseWedge = likelyReverseWedge; + this.stickyKeepOuterUntil90Degrees = stickyKeepOuterUntil90Degrees; + this.stickyKeepInnerUntil45Degrees = stickyKeepInnerUntil45Degrees; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Input)) return false; + Input that = (Input) o; + return likelyTentOrWedge == that.likelyTentOrWedge + && likelyReverseWedge == that.likelyReverseWedge + && stickyKeepOuterUntil90Degrees == that.stickyKeepOuterUntil90Degrees + && stickyKeepInnerUntil45Degrees == that.stickyKeepInnerUntil45Degrees + && hingeAngle == that.hingeAngle; + } + + @Override + public int hashCode() { + return Objects.hash(hingeAngle, likelyTentOrWedge, likelyReverseWedge, + stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees); + } + + @Override + public String toString() { + return "InputState{" + + "hingeAngle=" + hingeAngle + + ", likelyTentOrWedge=" + likelyTentOrWedge + + ", likelyReverseWedge=" + likelyReverseWedge + + ", stickyKeepOuterUntil90Degrees=" + stickyKeepOuterUntil90Degrees + + ", stickyKeepInnerUntil45Degrees=" + stickyKeepInnerUntil45Degrees + + '}'; + } + } + + /** + * Class that holds a state of the calculator, it could be used to store the current + * state or to define the target (output) state based on some input in {@link StateTransition}. + */ + public static class State { + public Boolean stickyKeepOuterUntil90Degrees; + public Boolean stickyKeepInnerUntil45Degrees; + + PreferredScreen mPreferredScreen; + + public State(Boolean stickyKeepOuterUntil90Degrees, + Boolean stickyKeepInnerUntil45Degrees, + PreferredScreen preferredScreen) { + this.stickyKeepOuterUntil90Degrees = stickyKeepOuterUntil90Degrees; + this.stickyKeepInnerUntil45Degrees = stickyKeepInnerUntil45Degrees; + this.mPreferredScreen = preferredScreen; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof State)) return false; + State that = (State) o; + return Objects.equals(stickyKeepOuterUntil90Degrees, + that.stickyKeepOuterUntil90Degrees) && Objects.equals( + stickyKeepInnerUntil45Degrees, that.stickyKeepInnerUntil45Degrees) + && mPreferredScreen == that.mPreferredScreen; + } + + @Override + public int hashCode() { + return Objects.hash(stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees, + mPreferredScreen); + } + + @Override + public String toString() { + return "State{" + + "stickyKeepOuterUntil90Degrees=" + stickyKeepOuterUntil90Degrees + + ", stickyKeepInnerUntil90Degrees=" + stickyKeepInnerUntil45Degrees + + ", closedState=" + mPreferredScreen + + '}'; + } + } +} diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java new file mode 100644 index 000000000000..16daacb36693 --- /dev/null +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java @@ -0,0 +1,722 @@ +/* + * 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.server.policy; + +import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen; +import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle; +import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition; + +import java.util.ArrayList; +import java.util.List; + +/** + * Describes all possible state transitions for {@link BookStylePreferredScreenCalculator}. + * It contains a default configuration for a foldable device that has two screens: smaller outer + * screen which has portrait natural orientation and a larger inner screen and allows to use the + * device in tent mode or wedge mode. + * + * As the output state could affect calculating of the new state, it could potentially cause + * infinite loop and make the state never settle down. This could be avoided using automated test + * that checks all possible inputs and asserts that the final state is valid. + * See sample test for the default transitions in {@link BookStyleClosedStateCalculatorTest}. + * + * - Tent mode is defined as a posture when the device is partially opened and placed on the ground + * on the edges that are parallel to the hinge. + * - Wedge mode is when the device is partially opened and placed flat on the ground with the part + * of the device that doesn't have the display + * - Reverse wedge mode is when the device is partially opened and placed flat on the ground with + * the outer screen down, so the outer screen is not accessible + * + * Behavior description: + * - When unfolding with screens off we assume that no sensor data available except hinge angle + * (based on hall sensor), so we switch to the inner screen immediately + * + * - When unfolding when screen is 'on' we can check if we are likely in tent or wedge mode + * - If not likely tent/wedge mode or sensors data not available, then we unfold immediately + * After unfolding, the state of the inner screen 'on' is sticky between 0 and 45 degrees, so + * it won't jump back to the outer screen even if you move the phone into tent/wedge mode. The + * stickiness is reset after fully closing the device or unfolding past 45 degrees. + * - If likely tent or wedge mode, switch only at 90 degrees + * Tent/wedge mode is 'sticky' between 0 and 90 degrees, so it won't reset until you either + * fully close the device or unfold past 90 degrees. + * + * - When folding we can check if we are likely in reverse wedge mode + * - If not likely in reverse wedge mode or sensor data is not available we switch to the outer + * screen at 45 degrees and enable sticky tent/wedge mode as before, this allows to enter + * tent/wedge mode even if you are not on an even surface or holding phone in landscape + * - If likely in reverse wedge mode, switch to the outer screen only at 0 degrees to allow + * some use cases like using camera in this posture, the check happens after passing 45 degrees + * and inner screen becomes sticky turned 'on' until fully closing or unfolding past 45 degrees + */ +public class BookStyleStateTransitions { + + public static final List<StateTransition> DEFAULT_STATE_TRANSITIONS = new ArrayList<>(); + + static { + // region Angle 0 + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + // endregion + + // region Angle 0-45 + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ true, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ true, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + // endregion + + // region Angle 45-90 + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + // endregion + + // region Angle 90-180 + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + // endregion + } +} diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java new file mode 100644 index 000000000000..8d01b7a9c523 --- /dev/null +++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java @@ -0,0 +1,703 @@ +/* + * 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.server.policy; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.STATE_OFF; +import static android.view.Display.STATE_ON; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Instrumentation; +import android.content.res.Configuration; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.display.DisplayManager; +import android.hardware.input.InputSensorInfo; +import android.os.Handler; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.view.Display; +import android.view.Surface; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.devicestate.DeviceStateProvider; +import com.android.server.devicestate.DeviceStateProvider.Listener; +import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl; +import com.android.server.policy.feature.flags.Flags; + +import org.junit.Before; +import org.junit.Rule; +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; +import org.mockito.internal.util.reflection.FieldSetter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Unit tests for {@link BookStyleDeviceStatePolicy.Provider}. + * <p/> + * Run with <code>atest BookStyleDeviceStatePolicyTest</code>. + */ +@RunWith(AndroidTestingRunner.class) +public final class BookStyleDeviceStatePolicyTest { + + private static final int DEVICE_STATE_CLOSED = 0; + private static final int DEVICE_STATE_HALF_OPENED = 1; + private static final int DEVICE_STATE_OPENED = 2; + + @Captor + private ArgumentCaptor<Integer> mDeviceStateCaptor; + @Captor + private ArgumentCaptor<DisplayManager.DisplayListener> mDisplayListenerCaptor; + @Mock + private SensorManager mSensorManager; + @Mock + private InputSensorInfo mInputSensorInfo; + @Mock + private Listener mListener; + @Mock + DisplayManager mDisplayManager; + @Mock + private Display mDisplay; + + private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl(); + + private final Configuration mConfiguration = new Configuration(); + + private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); + + @Rule + public final TestableContext mContext = new TestableContext( + mInstrumentation.getTargetContext()); + + private Sensor mHallSensor; + private Sensor mOrientationSensor; + private Sensor mHingeAngleSensor; + private Sensor mLeftAccelerometer; + private Sensor mRightAccelerometer; + + private Map<Sensor, List<SensorEventListener>> mSensorEventListeners = new HashMap<>(); + private DeviceStateProvider mProvider; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_FOLDABLES_POSTURE_BASED_CLOSED_STATE, true); + mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_DUAL_DISPLAY_BLOCKING, true); + + when(mInputSensorInfo.getName()).thenReturn("hall-effect"); + mHallSensor = new Sensor(mInputSensorInfo); + when(mInputSensorInfo.getName()).thenReturn("hinge-angle"); + mHingeAngleSensor = new Sensor(mInputSensorInfo); + when(mInputSensorInfo.getName()).thenReturn("left-accelerometer"); + mLeftAccelerometer = new Sensor(mInputSensorInfo); + when(mInputSensorInfo.getName()).thenReturn("right-accelerometer"); + mRightAccelerometer = new Sensor(mInputSensorInfo); + when(mInputSensorInfo.getName()).thenReturn("orientation"); + mOrientationSensor = new Sensor(mInputSensorInfo); + + mContext.addMockSystemService(SensorManager.class, mSensorManager); + + when(mSensorManager.getDefaultSensor(eq(Sensor.TYPE_HINGE_ANGLE), eq(true))) + .thenReturn(mHingeAngleSensor); + when(mSensorManager.getDefaultSensor(eq(Sensor.TYPE_DEVICE_ORIENTATION))) + .thenReturn(mOrientationSensor); + + when(mDisplayManager.getDisplay(eq(DEFAULT_DISPLAY))).thenReturn(mDisplay); + mContext.addMockSystemService(DisplayManager.class, mDisplayManager); + + mContext.ensureTestableResources(); + when(mContext.getResources().getConfiguration()).thenReturn(mConfiguration); + + final List<Sensor> sensors = new ArrayList<>(); + sensors.add(mHallSensor); + sensors.add(mHingeAngleSensor); + sensors.add(mOrientationSensor); + sensors.add(mLeftAccelerometer); + sensors.add(mRightAccelerometer); + + when(mSensorManager.registerListener(any(), any(), anyInt(), any())).thenAnswer( + invocation -> { + final SensorEventListener listener = invocation.getArgument(0); + final Sensor sensor = invocation.getArgument(1); + addSensorListener(sensor, listener); + return true; + }); + when(mSensorManager.registerListener(any(), any(), anyInt())).thenAnswer( + invocation -> { + final SensorEventListener listener = invocation.getArgument(0); + final Sensor sensor = invocation.getArgument(1); + addSensorListener(sensor, listener); + return true; + }); + + doAnswer(invocation -> { + final SensorEventListener listener = invocation.getArgument(0); + final boolean[] removed = {false}; + mSensorEventListeners.forEach((sensor, sensorEventListeners) -> + removed[0] |= sensorEventListeners.remove(listener)); + + if (!removed[0]) { + throw new IllegalArgumentException( + "Trying to unregister listener " + listener + " that was not registered"); + } + + return null; + }).when(mSensorManager).unregisterListener(any(SensorEventListener.class)); + + doAnswer(invocation -> { + final SensorEventListener listener = invocation.getArgument(0); + final Sensor sensor = invocation.getArgument(1); + + boolean removed = mSensorEventListeners.get(sensor).remove(listener); + if (!removed) { + throw new IllegalArgumentException( + "Trying to unregister listener " + listener + + " that was not registered for sensor " + sensor); + } + + return null; + }).when(mSensorManager).unregisterListener(any(SensorEventListener.class), + any(Sensor.class)); + + try { + FieldSetter.setField(mHallSensor, mHallSensor.getClass() + .getDeclaredField("mStringType"), "com.google.sensor.hall_effect"); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + + when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL))) + .thenReturn(sensors); + + mInstrumentation.runOnMainSync(() -> mProvider = createProvider()); + + verify(mDisplayManager, atLeastOnce()).registerDisplayListener( + mDisplayListenerCaptor.capture(), nullable(Handler.class)); + setScreenOn(true); + } + + @Test + public void test_noSensorEventsYet_reportOpenedState() { + mProvider.setListener(mListener); + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_deviceClosedSensorEventsBecameAvailable_reportsClosedState() { + mProvider.setListener(mListener); + clearInvocations(mListener); + + sendHingeAngle(0f); + + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_hingeAngleClosed_reportsClosedState() { + sendHingeAngle(0f); + + mProvider.setListener(mListener); + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_hingeAngleFullyOpened_reportsOpenedState() { + sendHingeAngle(180f); + + mProvider.setListener(mListener); + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_unfoldingFromClosedToFullyOpened_reportsOpenedEvent() { + sendHingeAngle(0f); + mProvider.setListener(mListener); + clearInvocations(mListener); + + sendHingeAngle(180f); + + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_foldingFromFullyOpenToFullyClosed_movesToClosedState() { + sendHingeAngle(180f); + + sendHingeAngle(0f); + + mProvider.setListener(mListener); + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_slowUnfolding_reportsEventsInOrder() { + sendHingeAngle(0f); + mProvider.setListener(mListener); + + sendHingeAngle(5f); + sendHingeAngle(10f); + sendHingeAngle(60f); + sendHingeAngle(100f); + sendHingeAngle(180f); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_CLOSED, + DEVICE_STATE_HALF_OPENED, + DEVICE_STATE_OPENED + ); + } + + @Test + public void test_slowFolding_reportsEventsInOrder() { + sendHingeAngle(180f); + mProvider.setListener(mListener); + + sendHingeAngle(180f); + sendHingeAngle(100f); + sendHingeAngle(60f); + sendHingeAngle(10f); + sendHingeAngle(5f); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_OPENED, + DEVICE_STATE_HALF_OPENED, + DEVICE_STATE_CLOSED + ); + } + + @Test + public void test_hingeAngleOpen_screenOff_reportsHalfFolded() { + sendHingeAngle(0f); + setScreenOn(false); + mProvider.setListener(mListener); + + sendHingeAngle(10f); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_CLOSED, + DEVICE_STATE_HALF_OPENED + ); + } + + @Test + public void test_slowUnfoldingWithScreenOff_reportsEventsInOrder() { + sendHingeAngle(0f); + setScreenOn(false); + mProvider.setListener(mListener); + + sendHingeAngle(5f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(10f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(60f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(100f); + sendHingeAngle(180f); + assertLatestReportedState(DEVICE_STATE_OPENED); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_CLOSED, + DEVICE_STATE_HALF_OPENED, + DEVICE_STATE_OPENED + ); + } + + @Test + public void test_unfoldWithScreenOff_reportsHalfOpened() { + sendHingeAngle(0f); + setScreenOn(false); + mProvider.setListener(mListener); + + sendHingeAngle(5f); + sendHingeAngle(10f); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_CLOSED, + DEVICE_STATE_HALF_OPENED + ); + } + + @Test + public void test_slowUnfoldingAndFolding_reportsEventsInOrder() { + sendHingeAngle(0f); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + + // Started unfolding + sendHingeAngle(5f); + sendHingeAngle(30f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(60f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(100f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(180f); + assertLatestReportedState(DEVICE_STATE_OPENED); + + // Started folding + sendHingeAngle(100f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(60f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(30f); + assertLatestReportedState(DEVICE_STATE_CLOSED); + sendHingeAngle(5f); + assertLatestReportedState(DEVICE_STATE_CLOSED); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_CLOSED, + DEVICE_STATE_HALF_OPENED, + DEVICE_STATE_OPENED, + DEVICE_STATE_HALF_OPENED, + DEVICE_STATE_CLOSED + ); + } + + @Test + public void test_unfoldTo30Degrees_screenOnRightSideMostlyFlat_keepsClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(true); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture()); + } + + @Test + public void test_unfoldTo30Degrees_seascapeDeviceOrientation_keepsClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + sendDeviceOrientation(Surface.ROTATION_270); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture()); + } + + @Test + public void test_unfoldTo30Degrees_landscapeScreenRotation_keepsClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + sendScreenRotation(Surface.ROTATION_90); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture()); + } + + @Test + public void test_unfoldTo30Degrees_seascapeScreenRotation_keepsClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + sendScreenRotation(Surface.ROTATION_270); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture()); + } + + @Test + public void test_unfoldTo30Degrees_screenOnRightSideNotFlat_switchesToHalfOpenState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener).onStateChanged(DEVICE_STATE_HALF_OPENED); + } + + @Test + public void test_unfoldTo30Degrees_screenOffRightSideFlat_switchesToHalfOpenState() { + sendHingeAngle(0f); + setScreenOn(false); + // This sensor event should be ignored as screen is off + sendRightSideFlatSensorEvent(true); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener).onStateChanged(DEVICE_STATE_HALF_OPENED); + } + + @Test + public void test_unfoldTo60Degrees_andFoldTo10_switchesToClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + sendHingeAngle(60f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + clearInvocations(mListener); + + sendHingeAngle(10f); + + verify(mListener).onStateChanged(DEVICE_STATE_CLOSED); + } + + @Test + public void test_foldTo10AndUnfoldTo85Degrees_keepsClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + sendHingeAngle(180f); + assertLatestReportedState(DEVICE_STATE_OPENED); + sendHingeAngle(10f); + assertLatestReportedState(DEVICE_STATE_CLOSED); + + sendHingeAngle(85f); + + // Keeps 'tent'/'wedge' mode even when right side is not flat + // as user manually folded the device not all the way + assertLatestReportedState(DEVICE_STATE_CLOSED); + } + + @Test + public void test_foldTo0AndUnfoldTo85Degrees_doesNotKeepClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + sendHingeAngle(180f); + assertLatestReportedState(DEVICE_STATE_OPENED); + sendHingeAngle(0f); + assertLatestReportedState(DEVICE_STATE_CLOSED); + + sendHingeAngle(85f); + + // Do not enter 'tent'/'wedge' mode when right side is not flat + // as user fully folded the device before that + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + } + + @Test + public void test_foldTo10_leftSideIsFlat_keepsInnerScreenForReverseWedge() { + sendHingeAngle(180f); + sendLeftSideFlatSensorEvent(true); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_OPENED); + + sendHingeAngle(10f); + + // Keep the inner screen for reverse wedge mode (e.g. for astrophotography use case) + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + } + + @Test + public void test_foldTo10_leftSideIsNotFlat_switchesToOuterScreen() { + sendHingeAngle(180f); + sendLeftSideFlatSensorEvent(false); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_OPENED); + + sendHingeAngle(10f); + + // Do not keep the inner screen as it is not reverse wedge mode + assertLatestReportedState(DEVICE_STATE_CLOSED); + } + + @Test + public void test_foldTo10_noAccelerometerEvents_switchesToOuterScreen() { + sendHingeAngle(180f); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_OPENED); + + sendHingeAngle(10f); + + // Do not keep the inner screen as it is not reverse wedge mode + assertLatestReportedState(DEVICE_STATE_CLOSED); + } + + @Test + public void test_deviceClosed_screenIsOff_noSensorListeners() { + mProvider.setListener(mListener); + + sendHingeAngle(0f); + setScreenOn(false); + + assertNoListenersForSensor(mLeftAccelerometer); + assertNoListenersForSensor(mRightAccelerometer); + assertNoListenersForSensor(mOrientationSensor); + } + + @Test + public void test_deviceClosed_screenIsOn_doesNotListenForOneAccelerometer() { + mProvider.setListener(mListener); + + sendHingeAngle(0f); + setScreenOn(true); + + assertNoListenersForSensor(mLeftAccelerometer); + assertListensForSensor(mRightAccelerometer); + assertListensForSensor(mOrientationSensor); + } + + @Test + public void test_deviceOpened_screenIsOn_listensToSensors() { + mProvider.setListener(mListener); + + sendHingeAngle(180f); + setScreenOn(true); + + assertListensForSensor(mLeftAccelerometer); + assertListensForSensor(mRightAccelerometer); + assertListensForSensor(mOrientationSensor); + } + + private void assertLatestReportedState(int state) { + final ArgumentCaptor<Integer> integerCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mListener, atLeastOnce()).onStateChanged(integerCaptor.capture()); + assertEquals(state, integerCaptor.getValue().intValue()); + } + + private void sendHingeAngle(float angle) { + sendSensorEvent(mHingeAngleSensor, new float[]{angle}); + } + + private void sendDeviceOrientation(int orientation) { + sendSensorEvent(mOrientationSensor, new float[]{orientation}); + } + + private void sendScreenRotation(int rotation) { + when(mDisplay.getRotation()).thenReturn(rotation); + mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY)); + } + + private void sendRightSideFlatSensorEvent(boolean flat) { + sendAccelerometerFlatEvents(mRightAccelerometer, flat); + } + + private void sendLeftSideFlatSensorEvent(boolean flat) { + sendAccelerometerFlatEvents(mLeftAccelerometer, flat); + } + + private static final int ACCELEROMETER_EVENTS = 10; + + private void sendAccelerometerFlatEvents(Sensor sensor, boolean flat) { + final float[] values = flat ? new float[]{0.00021f, -0.00013f, 9.7899f} : + new float[]{6.124f, 4.411f, -1.7899f}; + // Send the same values multiple times to bypass noise filter + for (int i = 0; i < ACCELEROMETER_EVENTS; i++) { + sendSensorEvent(sensor, values); + } + } + + private void setScreenOn(boolean isOn) { + int state = isOn ? STATE_ON : STATE_OFF; + when(mDisplay.getState()).thenReturn(state); + mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY)); + } + + private void sendSensorEvent(Sensor sensor, float[] values) { + SensorEvent event = mock(SensorEvent.class); + event.sensor = sensor; + try { + FieldSetter.setField(event, event.getClass().getField("values"), + values); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + + List<SensorEventListener> listeners = mSensorEventListeners.get(sensor); + if (listeners != null) { + listeners.forEach(sensorEventListener -> sensorEventListener.onSensorChanged(event)); + } + } + + private void assertNoListenersForSensor(Sensor sensor) { + final List<SensorEventListener> listeners = mSensorEventListeners.getOrDefault(sensor, + new ArrayList<>()); + assertWithMessage("Expected no listeners for sensor " + sensor + " but found some").that( + listeners).isEmpty(); + } + + private void assertListensForSensor(Sensor sensor) { + final List<SensorEventListener> listeners = mSensorEventListeners.getOrDefault(sensor, + new ArrayList<>()); + assertWithMessage( + "Expected at least one listener for sensor " + sensor).that( + listeners).isNotEmpty(); + } + + private void addSensorListener(Sensor sensor, SensorEventListener listener) { + List<SensorEventListener> listeners = mSensorEventListeners.computeIfAbsent( + sensor, k -> new ArrayList<>()); + listeners.add(listener); + } + + private DeviceStateProvider createProvider() { + return new BookStyleDeviceStatePolicy(mFakeFeatureFlags, mContext, mHingeAngleSensor, + mHallSensor, mLeftAccelerometer, mRightAccelerometer, + /* closeAngleDegrees= */ null).getDeviceStateProvider(); + } +} diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java new file mode 100644 index 000000000000..ae05b3f5c121 --- /dev/null +++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java @@ -0,0 +1,81 @@ +/* + * 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.server.policy; + + +import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.testing.AndroidTestingRunner; + +import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen; +import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle; + +import com.google.common.collect.Lists; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.List; + +/** + * Unit tests for {@link BookStylePreferredScreenCalculator}. + * <p/> + * Run with <code>atest BookStyleClosedStateCalculatorTest</code>. + */ +@RunWith(AndroidTestingRunner.class) +public final class BookStylePreferredScreenCalculatorTest { + + private final BookStylePreferredScreenCalculator mCalculator = + new BookStylePreferredScreenCalculator(DEFAULT_STATE_TRANSITIONS); + + private final List<HingeAngle> mHingeAngleValues = Arrays.asList(HingeAngle.values()); + private final List<Boolean> mLikelyTentModeValues = Arrays.asList(true, false); + private final List<Boolean> mLikelyReverseWedgeModeValues = Arrays.asList(true, false); + + @Test + public void transitionAllStates_noCrashes() { + final List<List<Object>> arguments = Lists.cartesianProduct(Arrays.asList( + mHingeAngleValues, + mLikelyTentModeValues, + mLikelyReverseWedgeModeValues + )); + + arguments.forEach(objects -> { + final HingeAngle hingeAngle = (HingeAngle) objects.get(0); + final boolean likelyTent = (boolean) objects.get(1); + final boolean likelyReverseWedge = (boolean) objects.get(2); + + final String description = + "Input: hinge angle = " + hingeAngle + ", likelyTent = " + likelyTent + + ", likelyReverseWedge = " + likelyReverseWedge; + + // Verify that there are no crashes because of infinite state transitions and + // that it returns a valid active state + try { + PreferredScreen preferredScreen = mCalculator.calculatePreferredScreen(hingeAngle, likelyTent, + likelyReverseWedge); + + assertWithMessage(description).that(preferredScreen).isNotEqualTo(PreferredScreen.INVALID); + } catch (Throwable exception) { + throw new AssertionError(description, exception); + } + }); + } +} |