Merge "Add foldables posture based closed device state" into main
diff --git a/services/foldables/devicestateprovider/proguard.flags b/services/foldables/devicestateprovider/proguard.flags
index 069cbc6..b810cad 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 0000000..d5a3cff
--- /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
similarity index 73%
rename from services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
rename to services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
index 5968b63..ad938af 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_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 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 @@
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 @@
* 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 @@
};
}
- 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 @@
);
}
+ 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 @@
}
@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 0000000..8977422
--- /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 0000000..16daacb
--- /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 0000000..8d01b7a
--- /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 0000000..ae05b3f
--- /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);
+ }
+ });
+ }
+}