Add support for sensor conditions in device state configuration file.

This change adds support for toggling device states based on sensor
values. For ex, a device with a hinge angle sensor can specify device
states based on the current hinge angle.

Test: atest DeviceStateProviderImplTest
Test: manual - place config on device and verify device state changes
with hinge angle sensor

Bug: 159401800
Change-Id: I41372c252fdc4c9d4f0306283cf260f793271195
This commit is contained in:
Darryl L Johnson
2020-11-16 17:57:17 -08:00
parent 1ddaa9f63f
commit 6a6ec1122e
6 changed files with 369 additions and 20 deletions

View File

@@ -100,7 +100,7 @@ public final class DeviceStateManagerService extends SystemService {
private final SparseArray<CallbackRecord> mCallbacks = new SparseArray<>();
public DeviceStateManagerService(@NonNull Context context) {
this(context, new DeviceStatePolicyImpl());
this(context, new DeviceStatePolicyImpl(context));
}
@VisibleForTesting

View File

@@ -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() {

View File

@@ -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<Conditions> 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<Sensor, SensorEvent> mLatestSensorEvent = new ArrayMap<>();
private DeviceStateProviderImpl(SparseArray<Conditions> conditionsForState) {
private DeviceStateProviderImpl(@NonNull Context context,
@NonNull SparseArray<Conditions> 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<Sensor> 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<BooleanSupplier> 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<SensorCondition> 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<Sensor> 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<NumericRange> mExpectedValues;
SensorBooleanSupplier(@NonNull Sensor sensor, @NonNull List<NumericRange> 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<BooleanSupplier> mBooleanSuppliers;
AndBooleanSupplier(@NonNull List<BooleanSupplier> 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.

View File

@@ -45,6 +45,7 @@
<xs:complexType name="conditions">
<xs:sequence>
<xs:element name="lid-switch" type="lidSwitchCondition" minOccurs="0" />
<xs:element name="sensor" type="sensorCondition" minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
@@ -54,4 +55,24 @@
</xs:sequence>
</xs:complexType>
<xs:complexType name="sensorCondition">
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="type" type="xs:positiveInteger" />
<xs:element name="value" type="numericRange" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="numericRange">
<xs:sequence>
<xs:choice minOccurs="0">
<xs:element name="min" type="xs:decimal" />
<xs:element name="min-inclusive" type="xs:decimal" />
</xs:choice>
<xs:choice minOccurs="0">
<xs:element name="max" type="xs:decimal" />
<xs:element name="max-inclusive" type="xs:decimal"/>
</xs:choice>
</xs:sequence>
</xs:complexType>
</xs:schema>

View File

@@ -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<com.android.server.policy.devicestate.config.SensorCondition> 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<com.android.server.policy.devicestate.config.NumericRange> 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;

View File

@@ -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<int[]> mIntArrayCaptor = ArgumentCaptor.forClass(int[].class);
private final ArgumentCaptor<Integer> 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 {
+ " </device-state>\n"
+ "</device-state-config>\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 = "<device-state-config>\n"
+ " <device-state>\n"
+ " <identifier>1</identifier>\n"
+ " <conditions>\n"
+ " <sensor>\n"
+ " <name>" + sensor.getName() + "</name>\n"
+ " <type>" + sensor.getType() + "</type>\n"
+ " <value>\n"
+ " <max>90</max>\n"
+ " </value>\n"
+ " </sensor>\n"
+ " </conditions>\n"
+ " </device-state>\n"
+ " <device-state>\n"
+ " <identifier>2</identifier>\n"
+ " <conditions>\n"
+ " <sensor>\n"
+ " <name>" + sensor.getName() + "</name>\n"
+ " <type>" + sensor.getType() + "</type>\n"
+ " <value>\n"
+ " <min-inclusive>90</min-inclusive>\n"
+ " <max>180</max>\n"
+ " </value>\n"
+ " </sensor>\n"
+ " </conditions>\n"
+ " </device-state>\n"
+ " <device-state>\n"
+ " <identifier>3</identifier>\n"
+ " <conditions>\n"
+ " <sensor>\n"
+ " <name>" + sensor.getName() + "</name>\n"
+ " <type>" + sensor.getType() + "</type>\n"
+ " <value>\n"
+ " <min-inclusive>180</min-inclusive>\n"
+ " </value>\n"
+ " </sensor>\n"
+ " </conditions>\n"
+ " </device-state>\n"
+ "</device-state-config>\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<Sensor> 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;