Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 26478200 authored by Orhan Uysal's avatar Orhan Uysal
Browse files

Block dual display when an external one is added

Introduce functionality for scenarios:
	- When an external display is connected, the dual display mode is not allowed
	- if user starts using the dual display mode and connects an external display, device reverts to single display mode.

Test: Manual
Test: atest FoldableDeviceStateProviderTest
Bug: 278667199
Change-Id: I92dd628548983a7ba8d04f53d76637944556b601
parent bcabede1
Loading
Loading
Loading
Loading
+15 −1
Original line number Diff line number Diff line
@@ -63,13 +63,27 @@ public interface DeviceStateProvider {
     */
    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 {}
+4 −1
Original line number Diff line number Diff line
@@ -5,9 +5,12 @@ package {
java_library {
    name: "foldable-device-state-provider",
    srcs: [
        "src/**/*.java"
        "src/**/*.java",
    ],
    libs: [
        "services",
    ],
    static_libs: [
        "device_state_flags_lib",
    ],
}
+144 −31
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STA
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.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.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.annotations.VisibleForTesting;
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,7 +61,7 @@ import java.util.function.Function;

/**
 * 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.
@@ -77,6 +83,13 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,
    // 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,
    @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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,
        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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,
            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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,
        }
    }

    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,21 +234,34 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,
            }
            listener = mListener;
            for (DeviceState deviceState : mOrderedStates) {
                if (isStateSupported(deviceState)) {
                    supportedStates.add(deviceState);
                }
            }
        }

        listener.onSupportedDeviceStatesChanged(
                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)) {
                    continue;
            return false;
        }
        if (mPowerSaveModeEnabled && deviceState.hasFlag(
                DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
                    continue;
                }
                supportedStates.add(deviceState);
            return false;
        }
        if (mIsDualDisplayBlockingEnabled
                && mStateAvailabilityConditions.contains(deviceState.getIdentifier())) {
            return mStateAvailabilityConditions
                    .get(deviceState.getIdentifier())
                    .getAsBoolean();
        }

        listener.onSupportedDeviceStatesChanged(
                supportedStates.toArray(new DeviceState[supportedStates.size()]), reason);
        return true;
    }

    /** Computes the current device state and notifies the listener of a change, if needed. */
@@ -307,12 +365,35 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,

    @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 final class FoldableDeviceStateProvider implements DeviceStateProvider,
     */
    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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,
                    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
@@ -434,6 +537,15 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,
        }
    }

    /**
     * @return Whether there is an external connected display.
     */
    public boolean hasNoConnectedExternalDisplay() {
        synchronized (mLock) {
            return mExternalDisplaysConnected.size() == 0;
        }
    }

    /**
     * @return Whether the screen is on.
     */
@@ -442,6 +554,7 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,
            return mIsScreenOn;
        }
    }

    /**
     * @return current hinge angle value of a foldable device
     */
+22 −7
Original line number Diff line number Diff line
@@ -33,6 +33,8 @@ import android.hardware.display.DisplayManager;
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 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy {

    private final DeviceStateProvider mProvider;

    private final boolean mIsDualDisplayBlockingEnabled;

    /**
     * Creates TentModeDeviceStatePolicy
     *
@@ -67,6 +71,12 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy {
     */
    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 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy {

        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 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy {
                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 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy {
                    DEVICE_STATE_CLOSED,
                    /* name= */ "CLOSED",
                    /* flags= */ FLAG_CANCEL_OVERRIDE_REQUESTS,
                    (provider) -> {
                    /* activeStatePredicate= */ (provider) -> {
                        final float hingeAngle = provider.getHingeAngle();
                        return hingeAngle <= closeAngleDegrees;
                    }
+12 −0
Original line number Diff line number Diff line
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",
}
Loading