diff --git a/services/core/Android.bp b/services/core/Android.bp index ff47a83b01153..4378cee66e54c 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 54f618327da88..396290ca9492a 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 85ab0bc12cae6..1e2f744f33667 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 android.annotation.Nullable; +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 fb55e75b9ac47..d1918d8dbe14c 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 0000000000000..a7b6b903a4385 --- /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 0000000000000..d6c6e31392ded --- /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 0000000000000..e69de29bb2d1d 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 0000000000000..e69de29bb2d1d 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 0000000000000..d802177e249b3 --- /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 0000000000000..a8a349e87b75e --- /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); + } + } +}