From 1ddaa9f63f4ea774e916c61e6fc70317fadce6d4 Mon Sep 17 00:00:00 2001 From: Darryl L Johnson Date: Wed, 4 Nov 2020 13:19:57 -0800 Subject: Update DeviceStateProviderImpl to read from device state config file. This change introduces the device_state_configuration.xml file schema and updates DeviceStateProviderImpl to read the set of supported states from the configuration file. The file schema and provider only support the lid switch condition, support for hinge angle will be added in a follow-up change. Test: atest DeviceStateProviderImplTest Test: manual - place config on device and verify device state changes with lid switch Bug: 159401800 Change-Id: I4472e23a5c8dbdacfe56aa2570eafa84033a7bfd --- services/core/Android.bp | 1 + .../server/policy/DeviceStatePolicyImpl.java | 2 +- .../server/policy/DeviceStateProviderImpl.java | 277 ++++++++++++++++++++- services/core/xsd/Android.bp | 7 + .../device-state-config/device-state-config.xsd | 57 +++++ .../xsd/device-state-config/schema/current.txt | 39 +++ .../device-state-config/schema/last_current.txt | 0 .../device-state-config/schema/last_removed.txt | 0 .../xsd/device-state-config/schema/removed.txt | 1 + .../server/policy/DeviceStateProviderImplTest.java | 165 ++++++++++++ 10 files changed, 537 insertions(+), 12 deletions(-) create mode 100644 services/core/xsd/device-state-config/device-state-config.xsd create mode 100644 services/core/xsd/device-state-config/schema/current.txt create mode 100644 services/core/xsd/device-state-config/schema/last_current.txt create mode 100644 services/core/xsd/device-state-config/schema/last_removed.txt create mode 100644 services/core/xsd/device-state-config/schema/removed.txt create mode 100644 services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java diff --git a/services/core/Android.bp b/services/core/Android.bp index ff47a83b0115..4378cee66e54 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -74,6 +74,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/policy/DeviceStatePolicyImpl.java b/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java index 54f618327da8..396290ca9492 100644 --- a/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java +++ b/services/core/java/com/android/server/policy/DeviceStatePolicyImpl.java @@ -30,7 +30,7 @@ public final class DeviceStatePolicyImpl implements DeviceStatePolicy { private final DeviceStateProvider mProvider; public DeviceStatePolicyImpl() { - mProvider = new DeviceStateProviderImpl(); + mProvider = DeviceStateProviderImpl.create(); } 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..1e2f744f3366 100644 --- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java +++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java @@ -16,30 +16,285 @@ package com.android.server.policy; +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; + +import android.annotation.NonNull; import android.annotation.Nullable; +import android.hardware.input.InputManagerInternal; +import android.os.Environment; +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.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.util.Arrays; +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 { + 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. */ + public static DeviceStateProviderImpl create() { + File configFile = getConfigurationFile(); + if (configFile == null) { + return createFromConfig(null); + } + return createFromConfig(new ReadableFileConfig(configFile)); + } + + /** + * Returns a new {@link DeviceStateProviderImpl} instance. + * + * @param readableConfig the config the provider instance should read supported states from. + */ + @VisibleForTesting + static DeviceStateProviderImpl createFromConfig(@Nullable ReadableConfig readableConfig) { + SparseArray 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(conditionsForState); + } + + // Lock for internal state. + private final Object mLock = new Object(); + // 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 mStateConditions; @Nullable + @GuardedBy("mLock") private Listener mListener = null; + @GuardedBy("mLock") + private int mLastReportedState = INVALID_DEVICE_STATE; + + @GuardedBy("mLock") + private boolean mIsLidOpen; + + private DeviceStateProviderImpl(SparseArray conditionsForState) { + 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; + + 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; + } + + 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; + } + + mStateConditions.put(state, new LidSwitchBooleanSupplier(lidSwitchCondition.getOpen())); + shouldListenToLidSwitch = true; + } + + if (shouldListenToLidSwitch) { + InputManagerInternal inputManager = LocalServices.getService( + InputManagerInternal.class); + inputManager.registerLidSwitchCallback(this); + } + } @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(); + } + + /** + * 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; + } + } + } + + /** + * Returns the device state configuration file that should be used, or {@code null} if no file + * is present on the device. + *

+ * 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..a7b6b903a438 --- /dev/null +++ b/services/core/xsd/device-state-config/device-state-config.xsd @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..d6c6e31392de --- /dev/null +++ b/services/core/xsd/device-state-config/schema/current.txt @@ -0,0 +1,39 @@ +// 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 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 getDeviceState(); + } + + public class LidSwitchCondition { + ctor public LidSwitchCondition(); + method public boolean getOpen(); + method public void setOpen(boolean); + } + + 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 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 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..a8a349e87b75 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java @@ -0,0 +1,165 @@ +/* + * 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 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.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.annotation.Nullable; +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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Unit tests for {@link DeviceStateProviderImpl}. + *

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