diff options
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); + } + } +} |