Merge "Block dual display when an external one is added" into main
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
index af33de0..50ab3f8 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
@@ -63,13 +63,27 @@
      */
     int SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED = 5;
 
+    /**
+     * Indicating that the supported device states have changed because an external display was
+     * added.
+     */
+    int SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED = 6;
+
+    /**
+     * Indicating that the supported device states have changed because an external display was
+     * removed.
+     */
+    int SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED = 7;
+
     @IntDef(prefix = { "SUPPORTED_DEVICE_STATES_CHANGED_" }, value = {
             SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT,
             SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED,
             SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL,
             SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL,
             SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED,
-            SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED
+            SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED,
+            SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED,
+            SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface SupportedStatesUpdatedReason {}
diff --git a/services/foldables/devicestateprovider/Android.bp b/services/foldables/devicestateprovider/Android.bp
index 34737ef..56daea7 100644
--- a/services/foldables/devicestateprovider/Android.bp
+++ b/services/foldables/devicestateprovider/Android.bp
@@ -5,9 +5,12 @@
 java_library {
     name: "foldable-device-state-provider",
     srcs: [
-        "src/**/*.java"
+        "src/**/*.java",
     ],
     libs: [
         "services",
     ],
+    static_libs: [
+        "device_state_flags_lib",
+    ],
 }
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index aea46d1..d13b543 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -21,6 +21,7 @@
 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 static android.view.Display.TYPE_EXTERNAL;
 
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -33,11 +34,14 @@
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
-import android.os.PowerManager;
 import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
 import android.os.Trace;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.view.Display;
 
 import com.android.internal.annotations.GuardedBy;
@@ -45,6 +49,8 @@
 import com.android.internal.util.Preconditions;
 import com.android.server.devicestate.DeviceState;
 import com.android.server.devicestate.DeviceStateProvider;
+import com.android.server.policy.feature.flags.FeatureFlags;
+import com.android.server.policy.feature.flags.FeatureFlagsImpl;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -55,14 +61,14 @@
 
 /**
  * Device state provider for foldable devices.
- *
+ * <p>
  * 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  {
+        DisplayManager.DisplayListener {
 
     private static final String TAG = "FoldableDeviceStateProvider";
     private static final boolean DEBUG = false;
@@ -77,6 +83,13 @@
     // are met for the device to be in the state.
     private final SparseArray<BooleanSupplier> mStateConditions = new SparseArray<>();
 
+    // Map of state identifier to a boolean supplier that returns true when the device state has all
+    // the conditions needed for availability.
+    private final SparseArray<BooleanSupplier> mStateAvailabilityConditions = new SparseArray<>();
+
+    @GuardedBy("mLock")
+    private final SparseBooleanArray mExternalDisplaysConnected = new SparseBooleanArray();
+
     private final Sensor mHingeAngleSensor;
     private final DisplayManager mDisplayManager;
     private final Sensor mHallSensor;
@@ -99,7 +112,23 @@
     @GuardedBy("mLock")
     private boolean mPowerSaveModeEnabled;
 
-    public FoldableDeviceStateProvider(@NonNull Context context,
+    private final boolean mIsDualDisplayBlockingEnabled;
+
+    public FoldableDeviceStateProvider(
+            @NonNull Context context,
+            @NonNull SensorManager sensorManager,
+            @NonNull Sensor hingeAngleSensor,
+            @NonNull Sensor hallSensor,
+            @NonNull DisplayManager displayManager,
+            @NonNull DeviceStateConfiguration[] deviceStateConfigurations) {
+        this(new FeatureFlagsImpl(), context, sensorManager, hingeAngleSensor, hallSensor,
+                displayManager, deviceStateConfigurations);
+    }
+
+    @VisibleForTesting
+    public FoldableDeviceStateProvider(
+            @NonNull FeatureFlags featureFlags,
+            @NonNull Context context,
             @NonNull SensorManager sensorManager,
             @NonNull Sensor hingeAngleSensor,
             @NonNull Sensor hallSensor,
@@ -112,6 +141,7 @@
         mHingeAngleSensor = hingeAngleSensor;
         mHallSensor = hallSensor;
         mDisplayManager = displayManager;
+        mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
 
         sensorManager.registerListener(this, mHingeAngleSensor, SENSOR_DELAY_FASTEST);
         sensorManager.registerListener(this, mHallSensor, SENSOR_DELAY_FASTEST);
@@ -121,20 +151,15 @@
             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));
+            assertUniqueDeviceStateIdentifier(configuration);
+            initialiseStateConditions(configuration);
+            initialiseStateAvailabilityConditions(configuration);
         }
 
+        Handler handler = new Handler(Looper.getMainLooper());
         mDisplayManager.registerDisplayListener(
                 /* listener = */ this,
-                /* handler= */ null,
-                /* eventsMask= */ DisplayManager.EVENT_FLAG_DISPLAY_CHANGED);
+                /* handler= */ handler);
 
         Arrays.sort(mOrderedStates, Comparator.comparingInt(DeviceState::getIdentifier));
 
@@ -167,6 +192,26 @@
         }
     }
 
+    private void assertUniqueDeviceStateIdentifier(DeviceStateConfiguration configuration) {
+        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());
+        }
+    }
+
+    private void initialiseStateConditions(DeviceStateConfiguration configuration) {
+        mStateConditions.put(configuration.mDeviceState.getIdentifier(), () ->
+                configuration.mActiveStatePredicate.apply(this));
+    }
+
+    private void initialiseStateAvailabilityConditions(DeviceStateConfiguration configuration) {
+        if (configuration.mAvailabilityPredicate != null) {
+            mStateAvailabilityConditions.put(configuration.mDeviceState.getIdentifier(), () ->
+                    configuration.mAvailabilityPredicate.apply(this));
+        }
+    }
+
     @Override
     public void setListener(Listener listener) {
         synchronized (mLock) {
@@ -189,16 +234,9 @@
             }
             listener = mListener;
             for (DeviceState deviceState : mOrderedStates) {
-                if (isThermalStatusCriticalOrAbove(mThermalStatus)
-                        && deviceState.hasFlag(
-                        DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
-                    continue;
+                if (isStateSupported(deviceState)) {
+                    supportedStates.add(deviceState);
                 }
-                if (mPowerSaveModeEnabled && deviceState.hasFlag(
-                        DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
-                    continue;
-                }
-                supportedStates.add(deviceState);
             }
         }
 
@@ -206,6 +244,26 @@
                 supportedStates.toArray(new DeviceState[supportedStates.size()]), reason);
     }
 
+    @GuardedBy("mLock")
+    private boolean isStateSupported(DeviceState deviceState) {
+        if (isThermalStatusCriticalOrAbove(mThermalStatus)
+                && deviceState.hasFlag(
+                DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
+            return false;
+        }
+        if (mPowerSaveModeEnabled && deviceState.hasFlag(
+                DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
+            return false;
+        }
+        if (mIsDualDisplayBlockingEnabled
+                && mStateAvailabilityConditions.contains(deviceState.getIdentifier())) {
+            return mStateAvailabilityConditions
+                    .get(deviceState.getIdentifier())
+                    .getAsBoolean();
+        }
+        return true;
+    }
+
     /** Computes the current device state and notifies the listener of a change, if needed. */
     void notifyDeviceStateChangedIfNeeded() {
         int stateToReport = INVALID_DEVICE_STATE;
@@ -294,7 +352,7 @@
     private void dumpSensorValues() {
         Slog.i(TAG, "Sensor values:");
         dumpSensorValues("Hall Sensor", mHallSensor, mLastHallSensorEvent);
-        dumpSensorValues("Hinge Angle Sensor",mHingeAngleSensor, mLastHingeAngleSensorEvent);
+        dumpSensorValues("Hinge Angle Sensor", mHingeAngleSensor, mLastHingeAngleSensorEvent);
         Slog.i(TAG, "isScreenOn: " + isScreenOn());
     }
 
@@ -307,12 +365,35 @@
 
     @Override
     public void onDisplayAdded(int displayId) {
+        // TODO(b/312397262): consider virtual displays cases
+        synchronized (mLock) {
+            if (mIsDualDisplayBlockingEnabled
+                    && !mExternalDisplaysConnected.get(displayId, false)
+                    && mDisplayManager.getDisplay(displayId).getType() == TYPE_EXTERNAL) {
+                mExternalDisplaysConnected.put(displayId, true);
 
+                // Only update the supported state when going from 0 external display to 1
+                if (mExternalDisplaysConnected.size() == 1) {
+                    notifySupportedStatesChanged(
+                            SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED);
+                }
+            }
+        }
     }
 
     @Override
     public void onDisplayRemoved(int displayId) {
+        synchronized (mLock) {
+            if (mIsDualDisplayBlockingEnabled && mExternalDisplaysConnected.get(displayId, false)) {
+                mExternalDisplaysConnected.delete(displayId);
 
+                // Only update the supported states when going from 1 external display to 0
+                if (mExternalDisplaysConnected.size() == 0) {
+                    notifySupportedStatesChanged(
+                            SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED);
+                }
+            }
+        }
     }
 
     @Override
@@ -338,12 +419,22 @@
      */
     public static class DeviceStateConfiguration {
         private final DeviceState mDeviceState;
-        private final Function<FoldableDeviceStateProvider, Boolean> mPredicate;
+        private final Function<FoldableDeviceStateProvider, Boolean> mActiveStatePredicate;
+        private final Function<FoldableDeviceStateProvider, Boolean> mAvailabilityPredicate;
 
-        private DeviceStateConfiguration(DeviceState deviceState,
+        private DeviceStateConfiguration(
+                DeviceState deviceState,
                 Function<FoldableDeviceStateProvider, Boolean> predicate) {
+            this(deviceState, predicate, null);
+        }
+
+        private DeviceStateConfiguration(
+                DeviceState deviceState,
+                Function<FoldableDeviceStateProvider, Boolean> activeStatePredicate,
+                Function<FoldableDeviceStateProvider, Boolean> availabilityPredicate) {
             mDeviceState = deviceState;
-            mPredicate = predicate;
+            mActiveStatePredicate = activeStatePredicate;
+            mAvailabilityPredicate = availabilityPredicate;
         }
 
         public static DeviceStateConfiguration createConfig(
@@ -365,21 +456,33 @@
                     predicate);
         }
 
+        /** Create a configuration with availability 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,
+                Function<FoldableDeviceStateProvider, Boolean> availabilityPredicate
+        ) {
+            return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
+                predicate, availabilityPredicate);
+        }
+
         /**
          * Creates a device state configuration for a closed tent-mode aware state.
-         *
+         * <p>
          * 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.
-         *
+         * <p>
          * 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.
-         *
+         * <p>
          * 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
@@ -435,6 +538,15 @@
     }
 
     /**
+     * @return Whether there is an external connected display.
+     */
+    public boolean hasNoConnectedExternalDisplay() {
+        synchronized (mLock) {
+            return mExternalDisplaysConnected.size() == 0;
+        }
+    }
+
+    /**
      * @return Whether the screen is on.
      */
     public boolean isScreenOn() {
@@ -442,6 +554,7 @@
             return mIsScreenOn;
         }
     }
+
     /**
      * @return current hinge angle value of a foldable device
      */
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
index 5f2cf3c..ddac88a 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
@@ -33,6 +33,8 @@
 import com.android.server.devicestate.DeviceStatePolicy;
 import com.android.server.devicestate.DeviceStateProvider;
 import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
+import com.android.server.policy.feature.flags.FeatureFlags;
+import com.android.server.policy.feature.flags.FeatureFlagsImpl;
 
 /**
  * Device state policy for a foldable device that supports tent mode: a mode when the device
@@ -55,6 +57,8 @@
 
     private final DeviceStateProvider mProvider;
 
+    private final boolean mIsDualDisplayBlockingEnabled;
+
     /**
      * Creates TentModeDeviceStatePolicy
      *
@@ -67,6 +71,12 @@
      */
     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) {
         super(context);
 
         final SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
@@ -74,8 +84,10 @@
 
         final DeviceStateConfiguration[] configuration = createConfiguration(closeAngleDegrees);
 
-        mProvider = new FoldableDeviceStateProvider(mContext, sensorManager, hingeAngleSensor,
-                hallSensor, displayManager, configuration);
+        mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
+
+        mProvider = new FoldableDeviceStateProvider(mContext, sensorManager,
+                hingeAngleSensor, hallSensor, displayManager, configuration);
     }
 
     private DeviceStateConfiguration[] createConfiguration(int closeAngleDegrees) {
@@ -83,24 +95,27 @@
                 createClosedConfiguration(closeAngleDegrees),
                 createConfig(DEVICE_STATE_HALF_OPENED,
                         /* name= */ "HALF_OPENED",
-                        (provider) -> {
+                        /* activeStatePredicate= */ (provider) -> {
                             final float hingeAngle = provider.getHingeAngle();
                             return hingeAngle >= MAX_CLOSED_ANGLE_DEGREES
                                     && hingeAngle <= TABLE_TOP_MODE_SWITCH_ANGLE_DEGREES;
                         }),
                 createConfig(DEVICE_STATE_OPENED,
                         /* name= */ "OPENED",
-                        (provider) -> true),
+                        /* activeStatePredicate= */ (provider) -> true),
                 createConfig(DEVICE_STATE_REAR_DISPLAY_STATE,
                         /* name= */ "REAR_DISPLAY_STATE",
                         /* flags= */ FLAG_EMULATED_ONLY,
-                        (provider) -> false),
+                        /* activeStatePredicate= */ (provider) -> false),
                 createConfig(DEVICE_STATE_CONCURRENT_INNER_DEFAULT,
                         /* name= */ "CONCURRENT_INNER_DEFAULT",
                         /* flags= */ FLAG_EMULATED_ONLY | FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP
                                 | FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
                                 | FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE,
-                        (provider) -> false)
+                        /* activeStatePredicate= */ (provider) -> false,
+                        /* availabilityPredicate= */
+                        provider -> !mIsDualDisplayBlockingEnabled
+                                || provider.hasNoConnectedExternalDisplay())
         };
     }
 
@@ -111,7 +126,7 @@
                     DEVICE_STATE_CLOSED,
                     /* name= */ "CLOSED",
                     /* flags= */ FLAG_CANCEL_OVERRIDE_REQUESTS,
-                    (provider) -> {
+                    /* activeStatePredicate= */ (provider) -> {
                         final float hingeAngle = provider.getHingeAngle();
                         return hingeAngle <= closeAngleDegrees;
                     }
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
new file mode 100644
index 0000000..6ad8d79
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
@@ -0,0 +1,12 @@
+aconfig_declarations {
+    name: "device_state_flags",
+    package: "com.android.server.policy.feature.flags",
+    srcs: [
+        "device_state_flags.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "device_state_flags_lib",
+    aconfig_declarations: "device_state_flags",
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
new file mode 100644
index 0000000..47c2a1b
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.policy.feature.flags"
+
+flag {
+    name: "enable_dual_display_blocking"
+    namespace: "display_manager"
+    description: "Feature flag for dual display blocking"
+    bug: "278667199"
+}
\ 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
index 8fa4ce5..ddf4a08 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
@@ -17,18 +17,21 @@
 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 android.view.Display.TYPE_EXTERNAL;
+import static android.view.Display.TYPE_INTERNAL;
+
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED;
 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;
@@ -36,12 +39,11 @@
 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.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -51,20 +53,21 @@
 import android.hardware.SensorManager;
 import android.hardware.display.DisplayManager;
 import android.hardware.input.InputSensorInfo;
-import android.os.PowerManager;
 import android.os.Handler;
+import android.os.PowerManager;
 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 com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
+import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl;
+import com.android.server.policy.feature.flags.Flags;
 
 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;
@@ -95,10 +98,16 @@
     @Mock
     private DisplayManager mDisplayManager;
     private FoldableDeviceStateProvider mProvider;
+    @Mock
+    private Display mDefaultDisplay;
+    @Mock
+    private Display mExternalDisplay;
 
+    private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl();
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_DUAL_DISPLAY_BLOCKING, true);
 
         mHallSensor = new Sensor(mInputSensorInfo);
         mHingeAngleSensor = new Sensor(mInputSensorInfo);
@@ -473,6 +482,133 @@
         assertThat(mProvider.isScreenOn()).isFalse();
     }
 
+    @Test
+    public void test_dualScreenDisabledWhenExternalScreenIsConnected() throws Exception {
+        when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+        when(mDefaultDisplay.getType()).thenReturn(TYPE_INTERNAL);
+
+        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= */ "DUAL_DISPLAY", /* flags */ 0,
+                        (c) -> false, FoldableDeviceStateProvider::hasNoConnectedExternalDisplay));
+
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+
+        clearInvocations(listener);
+
+        when(mDisplayManager.getDisplays())
+                .thenReturn(new Display[]{mDefaultDisplay, mExternalDisplay});
+        when(mDisplayManager.getDisplay(1)).thenReturn(mExternalDisplay);
+        when(mExternalDisplay.getType()).thenReturn(TYPE_EXTERNAL);
+
+        // The DUAL_DISPLAY state should be disabled.
+        mProvider.onDisplayAdded(1);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+        assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */)}).inOrder();
+        clearInvocations(listener);
+
+        // The DUAL_DISPLAY state should be re-enabled.
+        when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+        mProvider.onDisplayRemoved(1);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED));
+        assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+    }
+
+    @Test
+    public void test_notifySupportedStatesChangedCalledOnlyOnInitialExternalScreenAddition() {
+        when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+        when(mDefaultDisplay.getType()).thenReturn(TYPE_INTERNAL);
+
+        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= */ "DUAL_DISPLAY", /* flags */ 0,
+                        (c) -> false, FoldableDeviceStateProvider::hasNoConnectedExternalDisplay));
+
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+
+        clearInvocations(listener);
+
+        addExternalDisplay(1);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+        addExternalDisplay(2);
+        addExternalDisplay(3);
+        addExternalDisplay(4);
+        verify(listener, times(1))
+                .onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+    }
+
+    @Test
+    public void hasNoConnectedDisplay_afterExternalDisplayAdded_returnsFalse() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE",
+                        /* flags= */0, (c) -> true,
+                        FoldableDeviceStateProvider::hasNoConnectedExternalDisplay)
+        );
+
+        addExternalDisplay(/* displayId */ 1);
+
+        assertThat(mProvider.hasNoConnectedExternalDisplay()).isFalse();
+    }
+
+    @Test
+    public void hasNoConnectedDisplay_afterExternalDisplayAddedAndRemoved_returnsTrue() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE",
+                        /* flags= */0, (c) -> true,
+                        FoldableDeviceStateProvider::hasNoConnectedExternalDisplay)
+        );
+
+        addExternalDisplay(/* displayId */ 1);
+        mProvider.onDisplayRemoved(1);
+
+        assertThat(mProvider.hasNoConnectedExternalDisplay()).isTrue();
+    }
+    private void addExternalDisplay(int displayId) {
+        when(mDisplayManager.getDisplay(displayId)).thenReturn(mExternalDisplay);
+        when(mExternalDisplay.getType()).thenReturn(TYPE_EXTERNAL);
+        mProvider.onDisplayAdded(displayId);
+    }
     private void setScreenOn(boolean isOn) {
         Display mockDisplay = mock(Display.class);
         int state = isOn ? STATE_ON : STATE_OFF;
@@ -508,12 +644,11 @@
     }
 
     private void createProvider(DeviceStateConfiguration... configurations) {
-        mProvider = new FoldableDeviceStateProvider(mContext, mSensorManager, mHingeAngleSensor,
-                mHallSensor, mDisplayManager, configurations);
+        mProvider = new FoldableDeviceStateProvider(mFakeFeatureFlags, mContext, mSensorManager,
+                mHingeAngleSensor, mHallSensor, mDisplayManager, configurations);
         verify(mDisplayManager)
                 .registerDisplayListener(
                         mDisplayListenerCaptor.capture(),
-                        nullable(Handler.class),
-                        anyLong());
+                        nullable(Handler.class));
     }
 }