summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/Android.bp1
-rw-r--r--services/core/java/com/android/server/devicestate/DeviceStateManagerService.java2
-rw-r--r--services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java7
-rw-r--r--services/core/java/com/android/server/policy/DeviceStateProviderImpl.java456
-rw-r--r--services/core/xsd/Android.bp7
-rw-r--r--services/core/xsd/device-state-config/device-state-config.xsd78
-rw-r--r--services/core/xsd/device-state-config/schema/current.txt61
-rw-r--r--services/core/xsd/device-state-config/schema/last_current.txt0
-rw-r--r--services/core/xsd/device-state-config/schema/last_removed.txt0
-rw-r--r--services/core/xsd/device-state-config/schema/removed.txt1
-rw-r--r--services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java289
11 files changed, 888 insertions, 14 deletions
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 3a137263d182..069a5ea3b32f 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -75,6 +75,7 @@ java_library_static {
":platform-compat-config",
":display-device-config",
":cec-config",
+ ":device-state-config",
"java/com/android/server/EventLogTags.logtags",
"java/com/android/server/am/EventLogTags.logtags",
"java/com/android/server/wm/EventLogTags.logtags",
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index 3172a04e9a3d..d7dcbde5692d 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<CallbackRecord> 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 54f618327da8..154f9a455a1a 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 = new DeviceStateProviderImpl();
+ 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 85ab0bc12cae..321bb8c0251d 100644
--- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
@@ -16,30 +16,464 @@
package com.android.server.policy;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
+
+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;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
import com.android.server.devicestate.DeviceStateProvider;
+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;
+
+import java.io.BufferedInputStream;
+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;
/**
- * Default implementation of {@link DeviceStateProvider}. Currently only supports
- * {@link #DEFAULT_DEVICE_STATE}.
- *
- * @see DeviceStatePolicyImpl
+ * Implementation of {@link DeviceStateProvider} that reads the set of supported device states
+ * from a configuration file provided at either /vendor/etc/devicestate or
+ * /data/system/devicestate/. By default, the provider supports {@link #DEFAULT_DEVICE_STATE} when
+ * no configuration is provided.
*/
-final class DeviceStateProviderImpl implements DeviceStateProvider {
- private static final int DEFAULT_DEVICE_STATE = 0;
+public final class DeviceStateProviderImpl implements DeviceStateProvider,
+ InputManagerInternal.LidSwitchCallback, SensorEventListener {
+ private static final String TAG = "DeviceStateProviderImpl";
+
+ private static final BooleanSupplier TRUE_BOOLEAN_SUPPLIER = () -> true;
+
+ @VisibleForTesting
+ static final int DEFAULT_DEVICE_STATE = 0;
+
+ private static final String VENDOR_CONFIG_FILE_PATH = "etc/devicestate/";
+ private static final String DATA_CONFIG_FILE_PATH = "system/devicestate/";
+ private static final String CONFIG_FILE_NAME = "device_state_configuration.xml";
+
+ /** Interface that allows reading the device state configuration. */
+ interface ReadableConfig {
+ @NonNull
+ InputStream openRead() throws IOException;
+ }
+
+ /**
+ * 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(context, null);
+ }
+ 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(@NonNull Context context,
+ @Nullable ReadableConfig readableConfig) {
+ SparseArray<Conditions> conditionsForState = new SparseArray<>();
+ if (readableConfig != null) {
+ DeviceStateConfig config = parseConfig(readableConfig);
+ if (config != null) {
+ for (DeviceState stateConfig : config.getDeviceState()) {
+ int state = stateConfig.getIdentifier().intValue();
+ Conditions conditions = stateConfig.getConditions();
+ conditionsForState.put(state, conditions);
+ }
+ }
+ }
+
+ if (conditionsForState.size() == 0) {
+ conditionsForState.put(DEFAULT_DEVICE_STATE, null);
+ }
+ 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
+ // the device to be in the state.
+ private final SparseArray<BooleanSupplier> mStateConditions;
@Nullable
+ @GuardedBy("mLock")
private Listener mListener = null;
+ @GuardedBy("mLock")
+ private int mLastReportedState = INVALID_DEVICE_STATE;
+
+ @GuardedBy("mLock")
+ private boolean mIsLidOpen;
+ @GuardedBy("mLock")
+ private final Map<Sensor, SensorEvent> mLatestSensorEvent = new ArrayMap<>();
+
+ 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);
+ }
+
+ // Whether or not this instance should register to receive lid switch notifications from
+ // InputManagerInternal. If there are no device state conditions that are based on the lid
+ // 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];
+ Conditions conditions = conditionsForState.get(state);
+ if (conditions == null) {
+ mStateConditions.put(state, TRUE_BOOLEAN_SUPPLIER);
+ continue;
+ }
+
+ List<BooleanSupplier> suppliers = new ArrayList<>();
+
+ LidSwitchCondition lidSwitchCondition = conditions.getLidSwitch();
+ if (lidSwitchCondition != null) {
+ suppliers.add(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) {
+ InputManagerInternal inputManager = LocalServices.getService(
+ 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
public void setListener(Listener listener) {
- if (mListener != null) {
- throw new RuntimeException("Provider already has a listener set.");
+ synchronized (mLock) {
+ if (mListener != null) {
+ throw new RuntimeException("Provider already has a listener set.");
+ }
+ mListener = listener;
+ }
+ notifySupportedStatesChanged();
+ notifyDeviceStateChangedIfNeeded();
+ }
+
+ /** Notifies the listener that the set of supported device states has changed. */
+ private void notifySupportedStatesChanged() {
+ int[] supportedStates;
+ synchronized (mLock) {
+ if (mListener == null) {
+ return;
+ }
+
+ supportedStates = Arrays.copyOf(mOrderedStates, mOrderedStates.length);
}
- mListener = listener;
- mListener.onSupportedDeviceStatesChanged(new int[]{ DEFAULT_DEVICE_STATE });
- mListener.onStateChanged(DEFAULT_DEVICE_STATE);
+ mListener.onSupportedDeviceStatesChanged(supportedStates);
+ }
+
+ /** Computes the current device state and notifies the listener of a change, if needed. */
+ void notifyDeviceStateChangedIfNeeded() {
+ int stateToReport = INVALID_DEVICE_STATE;
+ synchronized (mLock) {
+ if (mListener == null) {
+ return;
+ }
+
+ int newState = mOrderedStates[0];
+ for (int i = 1; i < mOrderedStates.length; i++) {
+ int state = mOrderedStates[i];
+ if (mStateConditions.get(state).getAsBoolean()) {
+ newState = state;
+ break;
+ }
+ }
+
+ if (newState != mLastReportedState) {
+ mLastReportedState = newState;
+ stateToReport = newState;
+ }
+ }
+
+ if (stateToReport != INVALID_DEVICE_STATE) {
+ mListener.onStateChanged(stateToReport);
+ }
+ }
+
+ @Override
+ public void notifyLidSwitchChanged(long whenNanos, boolean lidOpen) {
+ synchronized (mLock) {
+ mIsLidOpen = lidOpen;
+ }
+ 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}.
+ */
+ private final class LidSwitchBooleanSupplier implements BooleanSupplier {
+ private final boolean mExpectedOpen;
+
+ LidSwitchBooleanSupplier(boolean expectedOpen) {
+ mExpectedOpen = expectedOpen;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ synchronized (mLock) {
+ return mIsLidOpen == mExpectedOpen;
+ }
+ }
+ }
+
+ /**
+ * 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.
+ * <p>
+ * Defaults to returning a config file present in the data/ dir at
+ * {@link #DATA_CONFIG_FILE_PATH}, and then falls back to the config file in the vendor/ dir
+ * at {@link #VENDOR_CONFIG_FILE_PATH} if no config file is found in the data/ dir.
+ */
+ @Nullable
+ private static File getConfigurationFile() {
+ final File configFileFromDataDir = Environment.buildPath(Environment.getDataDirectory(),
+ DATA_CONFIG_FILE_PATH, CONFIG_FILE_NAME);
+ if (configFileFromDataDir.exists()) {
+ return configFileFromDataDir;
+ }
+
+ final File configFileFromVendorDir = Environment.buildPath(Environment.getVendorDirectory(),
+ VENDOR_CONFIG_FILE_PATH, CONFIG_FILE_NAME);
+ if (configFileFromVendorDir.exists()) {
+ return configFileFromVendorDir;
+ }
+
+ return null;
+ }
+
+ /**
+ * Tries to parse the provided file into a {@link DeviceStateConfig} object. Returns
+ * {@code null} if the file could not be successfully parsed.
+ */
+ @Nullable
+ private static DeviceStateConfig parseConfig(@NonNull ReadableConfig readableConfig) {
+ try (InputStream in = readableConfig.openRead();
+ InputStream bin = new BufferedInputStream(in)) {
+ return XmlParser.read(bin);
+ } catch (IOException | DatatypeConfigurationException | XmlPullParserException e) {
+ Slog.e(TAG, "Encountered an error while reading device state config", e);
+ }
+ return null;
+ }
+
+ /** Implementation of {@link ReadableConfig} that reads config data from a file. */
+ private static final class ReadableFileConfig implements ReadableConfig {
+ @NonNull
+ private final File mFile;
+
+ private ReadableFileConfig(@NonNull File file) {
+ mFile = file;
+ }
+
+ @Override
+ public InputStream openRead() throws IOException {
+ return new FileInputStream(mFile);
+ }
}
}
diff --git a/services/core/xsd/Android.bp b/services/core/xsd/Android.bp
index fb55e75b9ac4..d1918d8dbe14 100644
--- a/services/core/xsd/Android.bp
+++ b/services/core/xsd/Android.bp
@@ -28,3 +28,10 @@ xsd_config {
api_dir: "cec-config/schema",
package_name: "com.android.server.hdmi.cec.config",
}
+
+xsd_config {
+ name: "device-state-config",
+ srcs: ["device-state-config/device-state-config.xsd"],
+ api_dir: "device-state-config/schema",
+ package_name: "com.android.server.policy.devicestate.config",
+}
diff --git a/services/core/xsd/device-state-config/device-state-config.xsd b/services/core/xsd/device-state-config/device-state-config.xsd
new file mode 100644
index 000000000000..0d8c08c93ff2
--- /dev/null
+++ b/services/core/xsd/device-state-config/device-state-config.xsd
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2020 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.
+ -->
+
+<xs:schema version="2.0"
+ elementFormDefault="qualified"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema">
+
+ <xs:element name="device-state-config">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="device-state" type="deviceState" maxOccurs="256" />
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+
+ <xs:complexType name="deviceState">
+ <xs:sequence>
+ <xs:element name="identifier">
+ <xs:simpleType>
+ <xs:restriction base="xs:integer">
+ <xs:minInclusive value="0" />
+ <xs:maxInclusive value="255" />
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:element>
+ <xs:element name="name" type="xs:string" minOccurs="0" />
+ <xs:element name="conditions" type="conditions" />
+ </xs:sequence>
+ </xs:complexType>
+
+ <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>
+
+ <xs:complexType name="lidSwitchCondition">
+ <xs:sequence>
+ <xs:element name="open" type="xs:boolean" />
+ </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>
diff --git a/services/core/xsd/device-state-config/schema/current.txt b/services/core/xsd/device-state-config/schema/current.txt
new file mode 100644
index 000000000000..667d1add5a98
--- /dev/null
+++ b/services/core/xsd/device-state-config/schema/current.txt
@@ -0,0 +1,61 @@
+// Signature format: 2.0
+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);
+ }
+
+ public class DeviceState {
+ ctor public DeviceState();
+ method public com.android.server.policy.devicestate.config.Conditions getConditions();
+ method public java.math.BigInteger getIdentifier();
+ method public String getName();
+ method public void setConditions(com.android.server.policy.devicestate.config.Conditions);
+ method public void setIdentifier(java.math.BigInteger);
+ method public void setName(String);
+ }
+
+ public class DeviceStateConfig {
+ ctor public DeviceStateConfig();
+ method public java.util.List<com.android.server.policy.devicestate.config.DeviceState> getDeviceState();
+ }
+
+ public class LidSwitchCondition {
+ ctor public LidSwitchCondition();
+ method public boolean getOpen();
+ 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;
+ method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+ method public static void skip(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+ }
+
+}
+
diff --git a/services/core/xsd/device-state-config/schema/last_current.txt b/services/core/xsd/device-state-config/schema/last_current.txt
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/services/core/xsd/device-state-config/schema/last_current.txt
diff --git a/services/core/xsd/device-state-config/schema/last_removed.txt b/services/core/xsd/device-state-config/schema/last_removed.txt
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/services/core/xsd/device-state-config/schema/last_removed.txt
diff --git a/services/core/xsd/device-state-config/schema/removed.txt b/services/core/xsd/device-state-config/schema/removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/services/core/xsd/device-state-config/schema/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
new file mode 100644
index 000000000000..92942bb91528
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2020 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.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;
+
+import com.android.server.LocalServices;
+import com.android.server.devicestate.DeviceStateProvider;
+
+import org.junit.After;
+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}.
+ * <p/>
+ * Run with <code>atest DeviceStateProviderImplTest</code>.
+ */
+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
+ public void tearDown() {
+ LocalServices.removeServiceForTest(InputManagerInternal.class);
+ }
+
+ @Test
+ public void create_noConfig() {
+ assertDefaultProviderValues(null);
+ }
+
+ @Test
+ public void create_emptyFile() {
+ String configString = "";
+ DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString);
+
+ assertDefaultProviderValues(config);
+ }
+
+ @Test
+ public void create_emptyConfig() {
+ String configString = "<device-state-config></device-state-config>";
+ DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString);
+
+ assertDefaultProviderValues(config);
+ }
+
+ @Test
+ public void create_invalidConfig() {
+ String configString = "<device-state-config>\n"
+ + " </device-state>\n"
+ + "</device-state-config>\n";
+ DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString);
+
+ assertDefaultProviderValues(config);
+ }
+
+ private void assertDefaultProviderValues(
+ @Nullable DeviceStateProviderImpl.ReadableConfig config) {
+ DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(mContext,
+ config);
+
+ DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
+ provider.setListener(listener);
+
+ verify(listener).onSupportedDeviceStatesChanged(mIntArrayCaptor.capture());
+ assertArrayEquals(new int[] { DEFAULT_DEVICE_STATE }, mIntArrayCaptor.getValue());
+
+ verify(listener).onStateChanged(mIntegerCaptor.capture());
+ assertEquals(DEFAULT_DEVICE_STATE, mIntegerCaptor.getValue().intValue());
+ }
+
+ @Test
+ public void create_lidSwitch() {
+ String configString = "<device-state-config>\n"
+ + " <device-state>\n"
+ + " <identifier>1</identifier>\n"
+ + " <conditions>\n"
+ + " <lid-switch>\n"
+ + " <open>true</open>\n"
+ + " </lid-switch>\n"
+ + " </conditions>\n"
+ + " </device-state>\n"
+ + " <device-state>\n"
+ + " <identifier>2</identifier>\n"
+ + " <conditions>\n"
+ + " <lid-switch>\n"
+ + " <open>false</open>\n"
+ + " </lid-switch>\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 }, mIntArrayCaptor.getValue());
+
+ verify(listener).onStateChanged(mIntegerCaptor.capture());
+ assertEquals(2, mIntegerCaptor.getValue().intValue());
+
+ Mockito.clearInvocations(listener);
+
+ provider.notifyLidSwitchChanged(0, true /* lidOpen */);
+
+ verify(listener, never()).onSupportedDeviceStatesChanged(mIntArrayCaptor.capture());
+ verify(listener).onStateChanged(mIntegerCaptor.capture());
+ 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;
+
+ TestReadableConfig(String configFileData) {
+ mData = configFileData.getBytes();
+ }
+
+ @NonNull
+ @Override
+ public InputStream openRead() throws IOException {
+ return new ByteArrayInputStream(mData);
+ }
+ }
+}