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

Commit 21f32069 authored by Nick Chameyev's avatar Nick Chameyev
Browse files

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
parent 7463f004
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
package {
    default_applicable_licenses: ["frameworks_base_license"],
}

java_library {
    name: "foldable-device-state-provider",
    srcs: [
        "src/**/*.java"
    ],
    libs: [
        "services",
    ],
}
+3 −0
Original line number Diff line number Diff line
# 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
+12 −0
Original line number Diff line number Diff line
{
  "presubmit": [
    {
      "name": "foldable-services-tests",
      "options": [
        {
          "exclude-annotation": "androidx.test.filters.FlakyTest"
        }
      ]
    }
  ]
}
+537 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+30 −0
Original line number Diff line number Diff line
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"]
}
Loading