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);
+            }
+        });
+    }
+}