diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index 3172a04e9a3d6..d7dcbde5692d5 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java @@ -100,7 +100,7 @@ public final class DeviceStateManagerService extends SystemService { private final SparseArray mCallbacks = new SparseArray<>(); public DeviceStateManagerService(@NonNull Context context) { - this(context, new DeviceStatePolicyImpl()); + this(context, new DeviceStatePolicyImpl(context)); } @VisibleForTesting diff --git a/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java b/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java index 396290ca9492a..154f9a455a1a5 100644 --- a/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java +++ b/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java @@ -17,6 +17,7 @@ package com.android.server.policy; import android.annotation.NonNull; +import android.content.Context; import com.android.server.devicestate.DeviceStatePolicy; import com.android.server.devicestate.DeviceStateProvider; @@ -27,10 +28,12 @@ import com.android.server.devicestate.DeviceStateProvider; * @see DeviceStateProviderImpl */ public final class DeviceStatePolicyImpl implements DeviceStatePolicy { + private final Context mContext; private final DeviceStateProvider mProvider; - public DeviceStatePolicyImpl() { - mProvider = DeviceStateProviderImpl.create(); + public DeviceStatePolicyImpl(Context context) { + mContext = context; + mProvider = DeviceStateProviderImpl.create(mContext); } public DeviceStateProvider getDeviceStateProvider() { diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java index 1e2f744f33667..321bb8c0251d4 100644 --- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java +++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java @@ -20,8 +20,15 @@ import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STA import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; import android.hardware.input.InputManagerInternal; import android.os.Environment; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; @@ -33,6 +40,8 @@ import com.android.server.policy.devicestate.config.Conditions; import com.android.server.policy.devicestate.config.DeviceState; import com.android.server.policy.devicestate.config.DeviceStateConfig; import com.android.server.policy.devicestate.config.LidSwitchCondition; +import com.android.server.policy.devicestate.config.NumericRange; +import com.android.server.policy.devicestate.config.SensorCondition; import com.android.server.policy.devicestate.config.XmlParser; import org.xmlpull.v1.XmlPullParserException; @@ -42,7 +51,11 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.function.BooleanSupplier; import javax.xml.datatype.DatatypeConfigurationException; @@ -54,7 +67,7 @@ import javax.xml.datatype.DatatypeConfigurationException; * no configuration is provided. */ public final class DeviceStateProviderImpl implements DeviceStateProvider, - InputManagerInternal.LidSwitchCallback { + InputManagerInternal.LidSwitchCallback, SensorEventListener { private static final String TAG = "DeviceStateProviderImpl"; private static final BooleanSupplier TRUE_BOOLEAN_SUPPLIER = () -> true; @@ -72,22 +85,28 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, InputStream openRead() throws IOException; } - /** Returns a new {@link DeviceStateProviderImpl} instance. */ - public static DeviceStateProviderImpl create() { + /** + * Returns a new {@link DeviceStateProviderImpl} instance. + * + * @param context the {@link Context} that should be used to access system services. + */ + public static DeviceStateProviderImpl create(@NonNull Context context) { File configFile = getConfigurationFile(); if (configFile == null) { - return createFromConfig(null); + return createFromConfig(context, null); } - return createFromConfig(new ReadableFileConfig(configFile)); + return createFromConfig(context, new ReadableFileConfig(configFile)); } /** * Returns a new {@link DeviceStateProviderImpl} instance. * + * @param context the {@link Context} that should be used to access system services. * @param readableConfig the config the provider instance should read supported states from. */ @VisibleForTesting - static DeviceStateProviderImpl createFromConfig(@Nullable ReadableConfig readableConfig) { + static DeviceStateProviderImpl createFromConfig(@NonNull Context context, + @Nullable ReadableConfig readableConfig) { SparseArray conditionsForState = new SparseArray<>(); if (readableConfig != null) { DeviceStateConfig config = parseConfig(readableConfig); @@ -103,11 +122,12 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, if (conditionsForState.size() == 0) { conditionsForState.put(DEFAULT_DEVICE_STATE, null); } - return new DeviceStateProviderImpl(conditionsForState); + return new DeviceStateProviderImpl(context, conditionsForState); } // Lock for internal state. private final Object mLock = new Object(); + private final Context mContext; // List of supported states in ascending order. private final int[] mOrderedStates; // Map of state to a boolean supplier that returns true when all required conditions are met for @@ -122,8 +142,12 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, @GuardedBy("mLock") private boolean mIsLidOpen; + @GuardedBy("mLock") + private final Map mLatestSensorEvent = new ArrayMap<>(); - private DeviceStateProviderImpl(SparseArray conditionsForState) { + private DeviceStateProviderImpl(@NonNull Context context, + @NonNull SparseArray conditionsForState) { + mContext = context; mOrderedStates = new int[conditionsForState.size()]; for (int i = 0; i < conditionsForState.size(); i++) { mOrderedStates[i] = conditionsForState.keyAt(i); @@ -134,6 +158,10 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, // switch there is no need to register for a callback. boolean shouldListenToLidSwitch = false; + final SensorManager sensorManager = mContext.getSystemService(SensorManager.class); + // The set of Sensor(s) that this instance should register to receive SensorEvent(s) from. + final ArraySet sensorsToListenTo = new ArraySet<>(); + mStateConditions = new SparseArray<>(); for (int i = 0; i < mOrderedStates.length; i++) { int state = mOrderedStates[i]; @@ -143,16 +171,48 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, continue; } + List suppliers = new ArrayList<>(); + LidSwitchCondition lidSwitchCondition = conditions.getLidSwitch(); - if (lidSwitchCondition == null) { - // We currently only support the lid switch so if it doesn't exist the condition - // is always true. - mStateConditions.put(state, TRUE_BOOLEAN_SUPPLIER); - continue; + if (lidSwitchCondition != null) { + suppliers.add(new LidSwitchBooleanSupplier(lidSwitchCondition.getOpen())); + shouldListenToLidSwitch = true; } - mStateConditions.put(state, new LidSwitchBooleanSupplier(lidSwitchCondition.getOpen())); - shouldListenToLidSwitch = true; + List sensorConditions = conditions.getSensor(); + for (int j = 0; j < sensorConditions.size(); j++) { + SensorCondition sensorCondition = sensorConditions.get(j); + final int expectedSensorType = sensorCondition.getType().intValue(); + final String expectedSensorName = sensorCondition.getName(); + + List sensors = sensorManager.getSensorList(expectedSensorType); + Sensor foundSensor = null; + for (int sensorIndex = 0; sensorIndex < sensors.size(); sensorIndex++) { + Sensor sensor = sensors.get(sensorIndex); + if (sensor.getName().equals(expectedSensorName)) { + foundSensor = sensor; + break; + } + } + + if (foundSensor == null) { + throw new IllegalStateException("Failed to find Sensor with type: " + + expectedSensorType + " and name: " + expectedSensorName); + } + + suppliers.add(new SensorBooleanSupplier(foundSensor, sensorCondition.getValue())); + sensorsToListenTo.add(foundSensor); + } + + if (suppliers.size() > 1) { + mStateConditions.put(state, new AndBooleanSupplier(suppliers)); + } else if (suppliers.size() > 0) { + // No need to wrap with an AND supplier if there is only 1. + mStateConditions.put(state, suppliers.get(0)); + } else { + // There are no conditions for this state. Default to always true. + mStateConditions.put(state, TRUE_BOOLEAN_SUPPLIER); + } } if (shouldListenToLidSwitch) { @@ -160,6 +220,11 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, InputManagerInternal.class); inputManager.registerLidSwitchCallback(this); } + + for (int i = 0; i < sensorsToListenTo.size(); i++) { + Sensor sensor = sensorsToListenTo.valueAt(i); + sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST); + } } @Override @@ -224,6 +289,19 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, notifyDeviceStateChangedIfNeeded(); } + @Override + public void onSensorChanged(SensorEvent event) { + synchronized (mLock) { + mLatestSensorEvent.put(event.sensor, event); + } + notifyDeviceStateChangedIfNeeded(); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // Do nothing. + } + /** * Implementation of {@link BooleanSupplier} that returns {@code true} if the expected lid * switch open state matches {@link #mIsLidOpen}. @@ -243,6 +321,107 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, } } + /** + * Implementation of {@link BooleanSupplier} that returns {@code true} if the latest + * {@link SensorEvent#values sensor event values} for the specified {@link Sensor} adhere to + * the supplied {@link NumericRange ranges}. + */ + private final class SensorBooleanSupplier implements BooleanSupplier { + @NonNull + private final Sensor mSensor; + @NonNull + private final List mExpectedValues; + + SensorBooleanSupplier(@NonNull Sensor sensor, @NonNull List expectedValues) { + mSensor = sensor; + mExpectedValues = expectedValues; + } + + @Override + public boolean getAsBoolean() { + synchronized (mLock) { + SensorEvent latestEvent = mLatestSensorEvent.get(mSensor); + if (latestEvent == null) { + // Default to returning false if we have not yet received a sensor event for the + // sensor. + return false; + } + + if (latestEvent.values.length != mExpectedValues.size()) { + throw new IllegalStateException("Number of supplied numeric range(s) does not " + + "match the number of values in the latest sensor event for sensor: " + + mSensor); + } + + for (int i = 0; i < latestEvent.values.length; i++) { + if (!adheresToRange(latestEvent.values[i], mExpectedValues.get(i))) { + return false; + } + } + return true; + } + } + + /** + * Returns {@code true} if the supplied {@code value} adheres to the constraints specified + * in {@code range}. + */ + private boolean adheresToRange(float value, @NonNull NumericRange range) { + final BigDecimal min = range.getMin_optional(); + if (min != null) { + if (value <= min.floatValue()) { + return false; + } + } + + final BigDecimal minInclusive = range.getMinInclusive_optional(); + if (minInclusive != null) { + if (value < minInclusive.floatValue()) { + return false; + } + } + + final BigDecimal max = range.getMax_optional(); + if (max != null) { + if (value >= max.floatValue()) { + return false; + } + } + + final BigDecimal maxInclusive = range.getMaxInclusive_optional(); + if (maxInclusive != null) { + if (value > maxInclusive.floatValue()) { + return false; + } + } + + return true; + } + } + + /** + * Implementation of {@link BooleanSupplier} whose result is the product of an AND operation + * applied to the result of all child suppliers. + */ + private static final class AndBooleanSupplier implements BooleanSupplier { + @NonNull + List mBooleanSuppliers; + + AndBooleanSupplier(@NonNull List booleanSuppliers) { + mBooleanSuppliers = booleanSuppliers; + } + + @Override + public boolean getAsBoolean() { + for (int i = 0; i < mBooleanSuppliers.size(); i++) { + if (!mBooleanSuppliers.get(i).getAsBoolean()) { + return false; + } + } + return true; + } + } + /** * Returns the device state configuration file that should be used, or {@code null} if no file * is present on the device. diff --git a/services/core/xsd/device-state-config/device-state-config.xsd b/services/core/xsd/device-state-config/device-state-config.xsd index a7b6b903a4385..0d8c08c93ff26 100644 --- a/services/core/xsd/device-state-config/device-state-config.xsd +++ b/services/core/xsd/device-state-config/device-state-config.xsd @@ -45,6 +45,7 @@ + @@ -54,4 +55,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/services/core/xsd/device-state-config/schema/current.txt b/services/core/xsd/device-state-config/schema/current.txt index d6c6e31392ded..667d1add5a98a 100644 --- a/services/core/xsd/device-state-config/schema/current.txt +++ b/services/core/xsd/device-state-config/schema/current.txt @@ -4,6 +4,7 @@ package com.android.server.policy.devicestate.config { public class Conditions { ctor public Conditions(); method public com.android.server.policy.devicestate.config.LidSwitchCondition getLidSwitch(); + method public java.util.List getSensor(); method public void setLidSwitch(com.android.server.policy.devicestate.config.LidSwitchCondition); } @@ -28,6 +29,27 @@ package com.android.server.policy.devicestate.config { method public void setOpen(boolean); } + public class NumericRange { + ctor public NumericRange(); + method public java.math.BigDecimal getMaxInclusive_optional(); + method public java.math.BigDecimal getMax_optional(); + method public java.math.BigDecimal getMinInclusive_optional(); + method public java.math.BigDecimal getMin_optional(); + method public void setMaxInclusive_optional(java.math.BigDecimal); + method public void setMax_optional(java.math.BigDecimal); + method public void setMinInclusive_optional(java.math.BigDecimal); + method public void setMin_optional(java.math.BigDecimal); + } + + public class SensorCondition { + ctor public SensorCondition(); + method public String getName(); + method public java.math.BigInteger getType(); + method public java.util.List getValue(); + method public void setName(String); + method public void setType(java.math.BigInteger); + } + public class XmlParser { ctor public XmlParser(); method public static com.android.server.policy.devicestate.config.DeviceStateConfig read(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException; diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java index a8a349e87b75e..92942bb915283 100644 --- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java +++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java @@ -17,15 +17,23 @@ package com.android.server.policy; +import static android.content.Context.SENSOR_SERVICE; + import static com.android.server.policy.DeviceStateProviderImpl.DEFAULT_DEVICE_STATE; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.annotation.Nullable; +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorManager; import android.hardware.input.InputManagerInternal; import androidx.annotation.NonNull; @@ -38,10 +46,13 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import org.mockito.internal.util.reflection.FieldSetter; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.util.List; /** * Unit tests for {@link DeviceStateProviderImpl}. @@ -52,9 +63,16 @@ public final class DeviceStateProviderImplTest { private final ArgumentCaptor mIntArrayCaptor = ArgumentCaptor.forClass(int[].class); private final ArgumentCaptor mIntegerCaptor = ArgumentCaptor.forClass(Integer.class); + private Context mContext; + private SensorManager mSensorManager; + @Before public void setup() { LocalServices.addService(InputManagerInternal.class, mock(InputManagerInternal.class)); + mContext = mock(Context.class); + mSensorManager = mock(SensorManager.class); + when(mContext.getSystemServiceName(eq(SensorManager.class))).thenReturn(SENSOR_SERVICE); + when(mContext.getSystemService(eq(SENSOR_SERVICE))).thenReturn(mSensorManager); } @After @@ -95,7 +113,8 @@ public final class DeviceStateProviderImplTest { private void assertDefaultProviderValues( @Nullable DeviceStateProviderImpl.ReadableConfig config) { - DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(config); + DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(mContext, + config); DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class); provider.setListener(listener); @@ -128,7 +147,8 @@ public final class DeviceStateProviderImplTest { + " \n" + "\n"; DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString); - DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(config); + DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(mContext, + config); DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class); provider.setListener(listener); @@ -148,6 +168,110 @@ public final class DeviceStateProviderImplTest { assertEquals(1, mIntegerCaptor.getValue().intValue()); } + @Test + public void create_sensor() throws Exception { + Sensor sensor = newSensor("sensor", Sensor.TYPE_HINGE_ANGLE); + when(mSensorManager.getSensorList(eq(sensor.getType()))).thenReturn(List.of(sensor)); + + String configString = "\n" + + " \n" + + " 1\n" + + " \n" + + " \n" + + " " + sensor.getName() + "\n" + + " " + sensor.getType() + "\n" + + " \n" + + " 90\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2\n" + + " \n" + + " \n" + + " " + sensor.getName() + "\n" + + " " + sensor.getType() + "\n" + + " \n" + + " 90\n" + + " 180\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 3\n" + + " \n" + + " \n" + + " " + sensor.getName() + "\n" + + " " + sensor.getType() + "\n" + + " \n" + + " 180\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString); + DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(mContext, + config); + + DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class); + provider.setListener(listener); + + verify(listener).onSupportedDeviceStatesChanged(mIntArrayCaptor.capture()); + assertArrayEquals(new int[] { 1, 2, 3 }, mIntArrayCaptor.getValue()); + + verify(listener).onStateChanged(mIntegerCaptor.capture()); + assertEquals(1, mIntegerCaptor.getValue().intValue()); + + Mockito.clearInvocations(listener); + + SensorEvent event0 = mock(SensorEvent.class); + event0.sensor = sensor; + FieldSetter.setField(event0, event0.getClass().getField("values"), new float[] { 180 }); + + provider.onSensorChanged(event0); + + verify(listener, never()).onSupportedDeviceStatesChanged(mIntArrayCaptor.capture()); + verify(listener).onStateChanged(mIntegerCaptor.capture()); + assertEquals(3, mIntegerCaptor.getValue().intValue()); + + Mockito.clearInvocations(listener); + + SensorEvent event1 = mock(SensorEvent.class); + event1.sensor = sensor; + FieldSetter.setField(event1, event1.getClass().getField("values"), new float[] { 90 }); + + provider.onSensorChanged(event1); + + verify(listener, never()).onSupportedDeviceStatesChanged(mIntArrayCaptor.capture()); + verify(listener).onStateChanged(mIntegerCaptor.capture()); + assertEquals(2, mIntegerCaptor.getValue().intValue()); + + Mockito.clearInvocations(listener); + + SensorEvent event2 = mock(SensorEvent.class); + event2.sensor = sensor; + FieldSetter.setField(event2, event2.getClass().getField("values"), new float[] { 0 }); + + provider.onSensorChanged(event2); + + verify(listener, never()).onSupportedDeviceStatesChanged(mIntArrayCaptor.capture()); + verify(listener).onStateChanged(mIntegerCaptor.capture()); + assertEquals(1, mIntegerCaptor.getValue().intValue()); + } + + private static Sensor newSensor(String name, int type) throws Exception { + Constructor constructor = Sensor.class.getDeclaredConstructor(); + constructor.setAccessible(true); + + Sensor sensor = constructor.newInstance(); + FieldSetter.setField(sensor, Sensor.class.getDeclaredField("mName"), name); + FieldSetter.setField(sensor, Sensor.class.getDeclaredField("mType"), type); + return sensor; + } + private static final class TestReadableConfig implements DeviceStateProviderImpl.ReadableConfig { private final byte[] mData;