Extract FoldableDeviceStateProvider to a library

Moves FoldableDeviceStateProvider to a separate
java library, so it could shared between
physical devices and cuttlefish devices.
Also makes it public (moves to AOSP).
No changes are made to the functionality,
this is just a move.

Bug: 284266229
Test: manual compile and check on a physical device
Test: presubmit
Flag: none
Change-Id: Ie7f7d334a4904bfbb19baa5dc814291828a8c9eb
diff --git a/services/foldables/devicestateprovider/Android.bp b/services/foldables/devicestateprovider/Android.bp
new file mode 100644
index 0000000..34737ef
--- /dev/null
+++ b/services/foldables/devicestateprovider/Android.bp
@@ -0,0 +1,13 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library {
+    name: "foldable-device-state-provider",
+    srcs: [
+        "src/**/*.java"
+    ],
+    libs: [
+        "services",
+    ],
+}
diff --git a/services/foldables/devicestateprovider/README.md b/services/foldables/devicestateprovider/README.md
new file mode 100644
index 0000000..90174c0
--- /dev/null
+++ b/services/foldables/devicestateprovider/README.md
@@ -0,0 +1,3 @@
+# Foldable Device State Provider library
+
+This library provides foldable-specific classes that could be used to implement a custom DeviceStateProvider.
\ No newline at end of file
diff --git a/services/foldables/devicestateprovider/TEST_MAPPING b/services/foldables/devicestateprovider/TEST_MAPPING
new file mode 100644
index 0000000..cd0d851
--- /dev/null
+++ b/services/foldables/devicestateprovider/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "foldable-services-tests",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
new file mode 100644
index 0000000..aea46d1
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -0,0 +1,537 @@
+/*
+ * 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_FASTEST;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
+import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
+import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.PowerManager;
+import android.hardware.display.DisplayManager;
+import android.os.Trace;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.Display;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.server.devicestate.DeviceState;
+import com.android.server.devicestate.DeviceStateProvider;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.BooleanSupplier;
+import java.util.function.Function;
+
+/**
+ * Device state provider for foldable devices.
+ *
+ * It is an implementation of {@link DeviceStateProvider} tailored specifically for
+ * foldable devices and allows simple callback-based configuration with hall sensor
+ * and hinge angle sensor values.
+ */
+public final class FoldableDeviceStateProvider implements DeviceStateProvider,
+        SensorEventListener, PowerManager.OnThermalStatusChangedListener,
+       DisplayManager.DisplayListener  {
+
+    private static final String TAG = "FoldableDeviceStateProvider";
+    private static final boolean DEBUG = false;
+
+    // Lock for internal state.
+    private final Object mLock = new Object();
+
+    // List of supported states in ascending order based on their identifier.
+    private final DeviceState[] mOrderedStates;
+
+    // Map of state identifier to a boolean supplier that returns true when all required conditions
+    // are met for the device to be in the state.
+    private final SparseArray<BooleanSupplier> mStateConditions = new SparseArray<>();
+
+    private final Sensor mHingeAngleSensor;
+    private final DisplayManager mDisplayManager;
+    private final Sensor mHallSensor;
+
+    @Nullable
+    @GuardedBy("mLock")
+    private Listener mListener = null;
+    @GuardedBy("mLock")
+    private int mLastReportedState = INVALID_DEVICE_STATE;
+    @GuardedBy("mLock")
+    private SensorEvent mLastHingeAngleSensorEvent = null;
+    @GuardedBy("mLock")
+    private SensorEvent mLastHallSensorEvent = null;
+    @GuardedBy("mLock")
+    private @PowerManager.ThermalStatus
+    int mThermalStatus = PowerManager.THERMAL_STATUS_NONE;
+    @GuardedBy("mLock")
+    private boolean mIsScreenOn = false;
+
+    @GuardedBy("mLock")
+    private boolean mPowerSaveModeEnabled;
+
+    public FoldableDeviceStateProvider(@NonNull Context context,
+            @NonNull SensorManager sensorManager,
+            @NonNull Sensor hingeAngleSensor,
+            @NonNull Sensor hallSensor,
+            @NonNull DisplayManager displayManager,
+            @NonNull DeviceStateConfiguration[] deviceStateConfigurations) {
+
+        Preconditions.checkArgument(deviceStateConfigurations.length > 0,
+                "Device state configurations array must not be empty");
+
+        mHingeAngleSensor = hingeAngleSensor;
+        mHallSensor = hallSensor;
+        mDisplayManager = displayManager;
+
+        sensorManager.registerListener(this, mHingeAngleSensor, SENSOR_DELAY_FASTEST);
+        sensorManager.registerListener(this, mHallSensor, SENSOR_DELAY_FASTEST);
+
+        mOrderedStates = new DeviceState[deviceStateConfigurations.length];
+        for (int i = 0; i < deviceStateConfigurations.length; i++) {
+            final DeviceStateConfiguration configuration = deviceStateConfigurations[i];
+            mOrderedStates[i] = configuration.mDeviceState;
+
+            if (mStateConditions.get(configuration.mDeviceState.getIdentifier()) != null) {
+                throw new IllegalArgumentException("Device state configurations must have unique"
+                        + " device state identifiers, found duplicated identifier: " +
+                        configuration.mDeviceState.getIdentifier());
+            }
+
+            mStateConditions.put(configuration.mDeviceState.getIdentifier(), () ->
+                    configuration.mPredicate.apply(this));
+        }
+
+        mDisplayManager.registerDisplayListener(
+                /* listener = */ this,
+                /* handler= */ null,
+                /* eventsMask= */ DisplayManager.EVENT_FLAG_DISPLAY_CHANGED);
+
+        Arrays.sort(mOrderedStates, Comparator.comparingInt(DeviceState::getIdentifier));
+
+        PowerManager powerManager = context.getSystemService(PowerManager.class);
+        if (powerManager != null) {
+            // If any of the device states are thermal sensitive, i.e. it should be disabled when
+            // the device is overheating, then we will update the list of supported states when
+            // thermal status changes.
+            if (hasThermalSensitiveState(deviceStateConfigurations)) {
+                powerManager.addThermalStatusListener(this);
+            }
+
+            // If any of the device states are power sensitive, i.e. it should be disabled when
+            // power save mode is enabled, then we will update the list of supported states when
+            // power save mode is toggled.
+            if (hasPowerSaveSensitiveState(deviceStateConfigurations)) {
+                IntentFilter filter = new IntentFilter(
+                        PowerManager.ACTION_POWER_SAVE_MODE_CHANGED_INTERNAL);
+                BroadcastReceiver receiver = new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED_INTERNAL.equals(
+                                intent.getAction())) {
+                            onPowerSaveModeChanged(powerManager.isPowerSaveMode());
+                        }
+                    }
+                };
+                context.registerReceiver(receiver, filter);
+            }
+        }
+    }
+
+    @Override
+    public void setListener(Listener listener) {
+        synchronized (mLock) {
+            if (mListener != null) {
+                throw new RuntimeException("Provider already has a listener set.");
+            }
+            mListener = listener;
+        }
+        notifySupportedStatesChanged(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED);
+        notifyDeviceStateChangedIfNeeded();
+    }
+
+    /** Notifies the listener that the set of supported device states has changed. */
+    private void notifySupportedStatesChanged(@SupportedStatesUpdatedReason int reason) {
+        List<DeviceState> supportedStates = new ArrayList<>();
+        Listener listener;
+        synchronized (mLock) {
+            if (mListener == null) {
+                return;
+            }
+            listener = mListener;
+            for (DeviceState deviceState : mOrderedStates) {
+                if (isThermalStatusCriticalOrAbove(mThermalStatus)
+                        && deviceState.hasFlag(
+                        DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
+                    continue;
+                }
+                if (mPowerSaveModeEnabled && deviceState.hasFlag(
+                        DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
+                    continue;
+                }
+                supportedStates.add(deviceState);
+            }
+        }
+
+        listener.onSupportedDeviceStatesChanged(
+                supportedStates.toArray(new DeviceState[supportedStates.size()]), reason);
+    }
+
+    /** Computes the current device state and notifies the listener of a change, if needed. */
+    void notifyDeviceStateChangedIfNeeded() {
+        int stateToReport = INVALID_DEVICE_STATE;
+        Listener listener;
+        synchronized (mLock) {
+            if (mListener == null) {
+                return;
+            }
+
+            listener = mListener;
+
+            int newState = INVALID_DEVICE_STATE;
+            for (int i = 0; i < mOrderedStates.length; i++) {
+                int state = mOrderedStates[i].getIdentifier();
+                if (DEBUG) {
+                    Slog.d(TAG, "Checking conditions for " + mOrderedStates[i].getName() + "("
+                            + i + ")");
+                }
+                boolean conditionSatisfied;
+                try {
+                    conditionSatisfied = mStateConditions.get(state).getAsBoolean();
+                } catch (IllegalStateException e) {
+                    // Failed to compute the current state based on current available data. Continue
+                    // with the expectation that notifyDeviceStateChangedIfNeeded() will be called
+                    // when a callback with the missing data is triggered. May trigger another state
+                    // change if another state is satisfied currently.
+                    Slog.w(TAG, "Unable to check current state = " + state, e);
+                    dumpSensorValues();
+                    continue;
+                }
+
+                if (conditionSatisfied) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Device State conditions satisfied, transition to " + state);
+                    }
+                    newState = state;
+                    break;
+                }
+            }
+            if (newState == INVALID_DEVICE_STATE) {
+                Slog.e(TAG, "No declared device states match any of the required conditions.");
+                dumpSensorValues();
+            }
+
+            if (newState != INVALID_DEVICE_STATE && newState != mLastReportedState) {
+                mLastReportedState = newState;
+                stateToReport = newState;
+            }
+        }
+
+        if (stateToReport != INVALID_DEVICE_STATE) {
+            listener.onStateChanged(stateToReport);
+        }
+    }
+
+    @Override
+    public void onSensorChanged(SensorEvent event) {
+        synchronized (mLock) {
+            if (event.sensor == mHallSensor) {
+                mLastHallSensorEvent = event;
+            } else if (event.sensor == mHingeAngleSensor) {
+                mLastHingeAngleSensorEvent = event;
+            }
+        }
+        notifyDeviceStateChangedIfNeeded();
+    }
+
+    @Override
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {
+        // Do nothing.
+    }
+
+    private float getSensorValue(@Nullable SensorEvent sensorEvent) {
+        if (sensorEvent == null) {
+            throw new IllegalStateException("Have not received sensor event.");
+        }
+
+        if (sensorEvent.values.length < 1) {
+            throw new IllegalStateException("Values in the sensor event are empty");
+        }
+
+        return sensorEvent.values[0];
+    }
+
+    @GuardedBy("mLock")
+    private void dumpSensorValues() {
+        Slog.i(TAG, "Sensor values:");
+        dumpSensorValues("Hall Sensor", mHallSensor, mLastHallSensorEvent);
+        dumpSensorValues("Hinge Angle Sensor",mHingeAngleSensor, mLastHingeAngleSensorEvent);
+        Slog.i(TAG, "isScreenOn: " + isScreenOn());
+    }
+
+    @GuardedBy("mLock")
+    private void dumpSensorValues(String sensorType, Sensor sensor, @Nullable SensorEvent event) {
+        String sensorString = sensor == null ? "null" : sensor.getName();
+        String eventValues = event == null ? "null" : Arrays.toString(event.values);
+        Slog.i(TAG, sensorType + " : " + sensorString + " : " + eventValues);
+    }
+
+    @Override
+    public void onDisplayAdded(int displayId) {
+
+    }
+
+    @Override
+    public void onDisplayRemoved(int displayId) {
+
+    }
+
+    @Override
+    public void onDisplayChanged(int displayId) {
+        if (displayId == DEFAULT_DISPLAY) {
+            // Could potentially be moved to the background if needed.
+            try {
+                Trace.beginSection("FoldableDeviceStateProvider#onDisplayChanged()");
+                int displayState = mDisplayManager.getDisplay(displayId).getState();
+                synchronized (mLock) {
+                    mIsScreenOn = displayState == Display.STATE_ON;
+                }
+            } finally {
+                Trace.endSection();
+            }
+        }
+    }
+
+    /**
+     * Configuration for a single device state, contains information about the state like
+     * identifier, name, flags and a predicate that should return true if the state should
+     * be selected.
+     */
+    public static class DeviceStateConfiguration {
+        private final DeviceState mDeviceState;
+        private final Function<FoldableDeviceStateProvider, Boolean> mPredicate;
+
+        private DeviceStateConfiguration(DeviceState deviceState,
+                Function<FoldableDeviceStateProvider, Boolean> predicate) {
+            mDeviceState = deviceState;
+            mPredicate = predicate;
+        }
+
+        public static DeviceStateConfiguration createConfig(
+                @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
+                @NonNull String name,
+                @DeviceState.DeviceStateFlags int flags,
+                Function<FoldableDeviceStateProvider, Boolean> predicate
+        ) {
+            return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
+                    predicate);
+        }
+
+        public static DeviceStateConfiguration createConfig(
+                @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
+                @NonNull String name,
+                Function<FoldableDeviceStateProvider, Boolean> predicate
+        ) {
+            return new DeviceStateConfiguration(new DeviceState(identifier, name, /* flags= */ 0),
+                    predicate);
+        }
+
+        /**
+         * Creates a device state configuration for a closed tent-mode aware state.
+         *
+         * During tent mode:
+         * - The inner display is OFF
+         * - The outer display is ON
+         * - The device is partially unfolded (left and right edges could be on the table)
+         * In this mode the device the device so it could be used in a posture where both left
+         * and right edges of the unfolded device are on the table.
+         *
+         * The predicate returns false after the hinge angle reaches
+         * {@code tentModeSwitchAngleDegrees}. Then it switches back only when the hinge angle
+         * becomes less than {@code maxClosedAngleDegrees}. Hinge angle is 0 degrees when the device
+         * is fully closed and 180 degrees when it is fully unfolded.
+         *
+         * For example, when tentModeSwitchAngleDegrees = 90 and maxClosedAngleDegrees = 5 degrees:
+         *  - when unfolding the device from fully closed posture (last state == closed or it is
+         *    undefined yet) this state will become not matching after reaching the angle
+         *    of 90 degrees, it allows the device to switch the outer display to the inner display
+         *    only when reaching this threshold
+         *  - when folding (last state != 'closed') this state will become matching after reaching
+         *    the angle less than 5 degrees and when hall sensor detected that the device is closed,
+         *    so the switch from the inner display to the outer will become only when the device
+         *    is fully closed.
+         *
+         * @param identifier state identifier
+         * @param name state name
+         * @param flags state flags
+         * @param minClosedAngleDegrees minimum (inclusive) hinge angle value for the closed state
+         * @param maxClosedAngleDegrees maximum (non-inclusive) hinge angle value for the closed
+         *                              state
+         * @param tentModeSwitchAngleDegrees the angle when this state should switch when unfolding
+         * @return device state configuration
+         */
+        public static DeviceStateConfiguration createTentModeClosedState(
+                @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
+                @NonNull String name,
+                @DeviceState.DeviceStateFlags int flags,
+                int minClosedAngleDegrees,
+                int maxClosedAngleDegrees,
+                int tentModeSwitchAngleDegrees
+        ) {
+            return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
+                    (stateContext) -> {
+                        final boolean hallSensorClosed = stateContext.isHallSensorClosed();
+                        final float hingeAngle = stateContext.getHingeAngle();
+                        final int lastState = stateContext.getLastReportedDeviceState();
+                        final boolean isScreenOn = stateContext.isScreenOn();
+
+                        final int switchingDegrees =
+                                isScreenOn ? tentModeSwitchAngleDegrees : maxClosedAngleDegrees;
+
+                        final int closedDeviceState = identifier;
+                        final boolean isLastStateClosed = lastState == closedDeviceState
+                                || lastState == INVALID_DEVICE_STATE;
+
+                        final boolean shouldBeClosedBecauseTentMode = isLastStateClosed
+                                && hingeAngle >= minClosedAngleDegrees
+                                && hingeAngle < switchingDegrees;
+
+                        final boolean shouldBeClosedBecauseFullyShut = hallSensorClosed
+                                && hingeAngle >= minClosedAngleDegrees
+                                && hingeAngle < maxClosedAngleDegrees;
+
+                        return shouldBeClosedBecauseFullyShut || shouldBeClosedBecauseTentMode;
+                    });
+        }
+    }
+
+    /**
+     * @return Whether the screen is on.
+     */
+    public boolean isScreenOn() {
+        synchronized (mLock) {
+            return mIsScreenOn;
+        }
+    }
+    /**
+     * @return current hinge angle value of a foldable device
+     */
+    public float getHingeAngle() {
+        synchronized (mLock) {
+            return getSensorValue(mLastHingeAngleSensorEvent);
+        }
+    }
+
+    /**
+     * @return true if hall sensor detected that the device is closed (fully shut)
+     */
+    public boolean isHallSensorClosed() {
+        synchronized (mLock) {
+            return getSensorValue(mLastHallSensorEvent) > 0f;
+        }
+    }
+
+    /**
+     * @return last reported device state
+     */
+    public int getLastReportedDeviceState() {
+        synchronized (mLock) {
+            return mLastReportedState;
+        }
+    }
+
+    @VisibleForTesting
+    void onPowerSaveModeChanged(boolean isPowerSaveModeEnabled) {
+        synchronized (mLock) {
+            if (mPowerSaveModeEnabled != isPowerSaveModeEnabled) {
+                mPowerSaveModeEnabled = isPowerSaveModeEnabled;
+                notifySupportedStatesChanged(
+                        isPowerSaveModeEnabled ? SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED
+                                : SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED);
+            }
+        }
+    }
+
+    @Override
+    public void onThermalStatusChanged(@PowerManager.ThermalStatus int thermalStatus) {
+        int previousThermalStatus;
+        synchronized (mLock) {
+            previousThermalStatus = mThermalStatus;
+            mThermalStatus = thermalStatus;
+        }
+
+        boolean isThermalStatusCriticalOrAbove = isThermalStatusCriticalOrAbove(thermalStatus);
+        boolean isPreviousThermalStatusCriticalOrAbove =
+                isThermalStatusCriticalOrAbove(previousThermalStatus);
+        if (isThermalStatusCriticalOrAbove != isPreviousThermalStatusCriticalOrAbove) {
+            Slog.i(TAG, "Updating supported device states due to thermal status change."
+                    + " isThermalStatusCriticalOrAbove: " + isThermalStatusCriticalOrAbove);
+            notifySupportedStatesChanged(
+                    isThermalStatusCriticalOrAbove
+                            ? SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL
+                            : SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL);
+        }
+    }
+
+    private static boolean isThermalStatusCriticalOrAbove(
+            @PowerManager.ThermalStatus int thermalStatus) {
+        switch (thermalStatus) {
+            case PowerManager.THERMAL_STATUS_CRITICAL:
+            case PowerManager.THERMAL_STATUS_EMERGENCY:
+            case PowerManager.THERMAL_STATUS_SHUTDOWN:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private static boolean hasThermalSensitiveState(DeviceStateConfiguration[] deviceStates) {
+        for (int i = 0; i < deviceStates.length; i++) {
+            DeviceStateConfiguration state = deviceStates[i];
+            if (state.mDeviceState
+                    .hasFlag(DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasPowerSaveSensitiveState(DeviceStateConfiguration[] deviceStates) {
+        for (int i = 0; i < deviceStates.length; i++) {
+            if (deviceStates[i].mDeviceState
+                    .hasFlag(DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/services/foldables/devicestateprovider/tests/Android.bp b/services/foldables/devicestateprovider/tests/Android.bp
new file mode 100644
index 0000000..a8db05e
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/Android.bp
@@ -0,0 +1,30 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "foldable-device-state-provider-tests",
+    srcs: ["src/**/*.java"],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+    ],
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libmultiplejvmtiagentsinterferenceagent",
+        "libstaticjvmtiagent",
+    ],
+    static_libs: [
+        "services",
+        "foldable-device-state-provider",
+        "androidx.test.rules",
+        "junit",
+        "truth-prebuilt",
+        "mockito-target-extended-minus-junit4",
+        "androidx.test.uiautomator_uiautomator",
+        "androidx.test.ext.junit",
+        "testables",
+    ],
+    test_suites: ["device-tests"]
+}
diff --git a/services/foldables/devicestateprovider/tests/AndroidManifest.xml b/services/foldables/devicestateprovider/tests/AndroidManifest.xml
new file mode 100644
index 0000000..736613d
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.foldablesdevicestatelib.tests">
+
+    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.foldablesdevicestatelib.tests"
+        android:label="Tests for foldable-device-state-provider library">
+    </instrumentation>
+
+</manifest>
\ No newline at end of file
diff --git a/services/foldables/devicestateprovider/tests/AndroidTest.xml b/services/foldables/devicestateprovider/tests/AndroidTest.xml
new file mode 100644
index 0000000..55e0d9c
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 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.
+  -->
+<configuration description="foldable-device-state-provider tests">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="foldable-device-state-provider-tests.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="com.android.foldableslib.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
new file mode 100644
index 0000000..8fa4ce5
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
@@ -0,0 +1,519 @@
+/*
+ * 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.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL;
+import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.STATE_OFF;
+import static android.view.Display.STATE_ON;
+
+import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.nullable;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.input.InputSensorInfo;
+import android.os.PowerManager;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+import android.view.Display;
+
+import com.android.server.devicestate.DeviceState;
+import com.android.server.devicestate.DeviceStateProvider;
+import com.android.server.devicestate.DeviceStateProvider.Listener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.internal.util.reflection.FieldSetter;
+
+/**
+ * Unit tests for {@link FoldableDeviceStateProvider}.
+ * <p/>
+ * Run with <code>atest FoldableDeviceStateProviderTest</code>.
+ */
+@RunWith(AndroidTestingRunner.class)
+public final class FoldableDeviceStateProviderTest {
+
+    private final ArgumentCaptor<DeviceState[]> mDeviceStateArrayCaptor = ArgumentCaptor.forClass(
+            DeviceState[].class);
+    @Captor
+    private ArgumentCaptor<Integer> mIntegerCaptor;
+    @Captor
+    private ArgumentCaptor<DisplayManager.DisplayListener> mDisplayListenerCaptor;
+    @Mock
+    private SensorManager mSensorManager;
+    @Mock
+    private Context mContext;
+    @Mock
+    private InputSensorInfo mInputSensorInfo;
+    private Sensor mHallSensor;
+    private Sensor mHingeAngleSensor;
+    @Mock
+    private DisplayManager mDisplayManager;
+    private FoldableDeviceStateProvider mProvider;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mHallSensor = new Sensor(mInputSensorInfo);
+        mHingeAngleSensor = new Sensor(mInputSensorInfo);
+    }
+
+    @Test
+    public void create_emptyConfiguration_throwsException() {
+        assertThrows(IllegalArgumentException.class, this::createProvider);
+    }
+
+    @Test
+    public void create_duplicatedDeviceStateIdentifiers_throwsException() {
+        assertThrows(IllegalArgumentException.class,
+                () -> createProvider(
+                        createConfig(
+                                /* identifier= */ 0, /* name= */ "ONE", (c) -> true),
+                        createConfig(
+                                /* identifier= */ 0, /* name= */ "TWO", (c) -> true)
+                ));
+    }
+
+    @Test
+    public void create_allMatchingStatesDefaultsToTheFirstIdentifier() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> true),
+                createConfig(
+                        /* identifier= */ 2, /* name= */ "TWO", (c) -> true),
+                createConfig(
+                        /* identifier= */ 3, /* name= */ "THREE", (c) -> true)
+        );
+
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        final DeviceState[] expectedStates = new DeviceState[]{
+                new DeviceState(1, "ONE", /* flags= */ 0),
+                new DeviceState(2, "TWO", /* flags= */ 0),
+                new DeviceState(3, "THREE", /* flags= */ 0),
+        };
+        assertArrayEquals(expectedStates, mDeviceStateArrayCaptor.getValue());
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(1, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void create_multipleMatchingStatesDefaultsToTheLowestIdentifier() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> false),
+                createConfig(
+                        /* identifier= */ 3, /* name= */ "THREE", (c) -> false),
+                createConfig(
+                        /* identifier= */ 4, /* name= */ "FOUR", (c) -> true),
+                createConfig(
+                        /* identifier= */ 2, /* name= */ "TWO", (c) -> true)
+        );
+
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hingeAngleUpdatedFirstTime_switchesToMatchingState() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 2, /* name= */ "TWO",
+                        (c) -> c.getHingeAngle() >= 90f));
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener, never()).onStateChanged(anyInt());
+        clearInvocations(listener);
+
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 100f);
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hallSensorUpdatedFirstTime_switchesToMatchingState() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> !c.isHallSensorClosed()),
+                createConfig(/* identifier= */ 2, /* name= */ "TWO",
+                        FoldableDeviceStateProvider::isHallSensorClosed));
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener, never()).onStateChanged(anyInt());
+        clearInvocations(listener);
+
+        // Hall sensor value '1f' is for the closed state
+        sendSensorEvent(mHallSensor, /* value= */ 1f);
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hingeAngleUpdatedSecondTime_switchesToMatchingState() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 2, /* name= */ "TWO",
+                        (c) -> c.getHingeAngle() >= 90f));
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 30f);
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(1, mIntegerCaptor.getValue().intValue());
+        clearInvocations(listener);
+
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 100f);
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hallSensorUpdatedSecondTime_switchesToMatchingState() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> !c.isHallSensorClosed()),
+                createConfig(/* identifier= */ 2, /* name= */ "TWO",
+                        FoldableDeviceStateProvider::isHallSensorClosed));
+        sendSensorEvent(mHallSensor, /* value= */ 0f);
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(1, mIntegerCaptor.getValue().intValue());
+        clearInvocations(listener);
+
+        // Hall sensor value '1f' is for the closed state
+        sendSensorEvent(mHallSensor, /* value= */ 1f);
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_invalidSensorValues_onStateChangedIsNotTriggered() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 2, /* name= */ "TWO",
+                        (c) -> c.getHingeAngle() >= 90f));
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        clearInvocations(listener);
+
+        // First, switch to a non-default state.
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 100f);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+
+        clearInvocations(listener);
+
+        // Then, send an invalid sensor event, verify that onStateChanged() is not triggered.
+        sendInvalidSensorEvent(mHingeAngleSensor);
+
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        verify(listener, never()).onStateChanged(mIntegerCaptor.capture());
+    }
+
+    @Test
+    public void test_nullSensorValues_noExceptionThrown() throws Exception {
+        createProvider(createConfig( /* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> c.getHingeAngle() < 90f));
+        sendInvalidSensorEvent(null);
+    }
+
+    @Test
+    public void test_flagDisableWhenThermalStatusCritical() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "CLOSED",
+                        (c) -> c.getHingeAngle() < 5f),
+                createConfig(/* identifier= */ 2, /* name= */ "HALF_OPENED",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 3, /* name= */ "OPENED",
+                        (c) -> c.getHingeAngle() < 180f),
+                createConfig(/* identifier= */ 4, /* name= */ "THERMAL_TEST",
+                        DeviceState.FLAG_EMULATED_ONLY
+                                | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE,
+                        (c) -> true));
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "THERMAL_TEST",
+                                DeviceState.FLAG_EMULATED_ONLY
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)},
+                mDeviceStateArrayCaptor.getValue());
+        clearInvocations(listener);
+
+        mProvider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_MODERATE);
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        clearInvocations(listener);
+
+        // The THERMAL_TEST state should be disabled.
+        mProvider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_CRITICAL);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */)},
+                mDeviceStateArrayCaptor.getValue());
+        clearInvocations(listener);
+
+        // The THERMAL_TEST state should be re-enabled.
+        mProvider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_LIGHT);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "THERMAL_TEST",
+                                DeviceState.FLAG_EMULATED_ONLY
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)},
+                mDeviceStateArrayCaptor.getValue());
+    }
+
+    @Test
+    public void test_flagDisableWhenPowerSaveEnabled() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "CLOSED",
+                        (c) -> c.getHingeAngle() < 5f),
+                createConfig(/* identifier= */ 2, /* name= */ "HALF_OPENED",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 3, /* name= */ "OPENED",
+                        (c) -> c.getHingeAngle() < 180f),
+                createConfig(/* identifier= */ 4, /* name= */ "THERMAL_TEST",
+                        DeviceState.FLAG_EMULATED_ONLY
+                                | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE,
+                        (c) -> true));
+        mProvider.onPowerSaveModeChanged(false /* isPowerSaveModeEnabled */);
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "THERMAL_TEST",
+                                DeviceState.FLAG_EMULATED_ONLY
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE) },
+                mDeviceStateArrayCaptor.getValue());
+        clearInvocations(listener);
+
+        mProvider.onPowerSaveModeChanged(false /* isPowerSaveModeEnabled */);
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        clearInvocations(listener);
+
+        // The THERMAL_TEST state should be disabled due to power save being enabled.
+        mProvider.onPowerSaveModeChanged(true /* isPowerSaveModeEnabled */);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */) },
+                mDeviceStateArrayCaptor.getValue());
+        clearInvocations(listener);
+
+        // The THERMAL_TEST state should be re-enabled.
+        mProvider.onPowerSaveModeChanged(false /* isPowerSaveModeEnabled */);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "THERMAL_TEST",
+                                DeviceState.FLAG_EMULATED_ONLY
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE) },
+                mDeviceStateArrayCaptor.getValue());
+    }
+
+    @Test
+    public void test_previousStateBasedPredicate() {
+        // Create a configuration where state TWO could be matched only if
+        // the previous state was 'THREE'
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> c.getHingeAngle() < 30f),
+                createConfig(
+                        /* identifier= */ 2, /* name= */ "TWO",
+                        (c) -> c.getLastReportedDeviceState() == 3 && c.getHingeAngle() > 120f),
+                createConfig(
+                        /* identifier= */ 3, /* name= */ "THREE",
+                        (c) -> c.getHingeAngle() > 90f)
+        );
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 0f);
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+
+        // Check that the initial state is 'ONE'
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(1, mIntegerCaptor.getValue().intValue());
+        clearInvocations(listener);
+
+        // Should not match state 'TWO', it should match only state 'THREE'
+        // (because the previous state is not 'THREE', it is 'ONE')
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 180f);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(3, mIntegerCaptor.getValue().intValue());
+        clearInvocations(listener);
+
+        // Now it should match state 'TWO'
+        // (because the previous state is 'THREE' now)
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 180f);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void isScreenOn_afterDisplayChangedToOn_returnsTrue() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> true)
+        );
+
+        setScreenOn(true);
+
+        assertThat(mProvider.isScreenOn()).isTrue();
+    }
+
+    @Test
+    public void isScreenOn_afterDisplayChangedToOff_returnsFalse() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> true)
+        );
+
+        setScreenOn(false);
+
+        assertThat(mProvider.isScreenOn()).isFalse();
+    }
+
+    @Test
+    public void isScreenOn_afterDisplayChangedToOnThenOff_returnsFalse() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> true)
+        );
+
+        setScreenOn(true);
+        setScreenOn(false);
+
+        assertThat(mProvider.isScreenOn()).isFalse();
+    }
+
+    private void setScreenOn(boolean isOn) {
+        Display mockDisplay = mock(Display.class);
+        int state = isOn ? STATE_ON : STATE_OFF;
+        when(mockDisplay.getState()).thenReturn(state);
+        when(mDisplayManager.getDisplay(eq(DEFAULT_DISPLAY))).thenReturn(mockDisplay);
+        mDisplayListenerCaptor.getValue().onDisplayChanged(DEFAULT_DISPLAY);
+    }
+
+    private void sendSensorEvent(Sensor sensor, float value) {
+        SensorEvent event = mock(SensorEvent.class);
+        event.sensor = sensor;
+        try {
+            FieldSetter.setField(event, event.getClass().getField("values"),
+                    new float[]{value});
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        }
+
+        mProvider.onSensorChanged(event);
+    }
+
+    private void sendInvalidSensorEvent(Sensor sensor) {
+        SensorEvent event = mock(SensorEvent.class);
+        event.sensor = sensor;
+        try {
+            // Set empty values array to make the event invalid
+            FieldSetter.setField(event, event.getClass().getField("values"),
+                    new float[]{});
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        }
+        mProvider.onSensorChanged(event);
+    }
+
+    private void createProvider(DeviceStateConfiguration... configurations) {
+        mProvider = new FoldableDeviceStateProvider(mContext, mSensorManager, mHingeAngleSensor,
+                mHallSensor, mDisplayManager, configurations);
+        verify(mDisplayManager)
+                .registerDisplayListener(
+                        mDisplayListenerCaptor.capture(),
+                        nullable(Handler.class),
+                        anyLong());
+    }
+}