blob: edd5f5f415c62901064dcbc0fab3c2fee63af77d [file] [log] [blame]
/*
* 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.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_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.internal.util.Preconditions;
import com.android.server.LocalServices;
import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.policy.devicestate.config.Conditions;
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.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.BooleanSupplier;
import javax.xml.datatype.DatatypeConfigurationException;
/**
* 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/.
* <p>
* When a device state configuration file is present this provider will consider the provided
* {@link Conditions} block for each declared state, halting and returning when the first set of
* conditions for a device state match the current system state. If there are multiple states whose
* conditions match the current system state the matching state with the smallest integer identifier
* will be returned. When no declared state matches the current system state, the device state with
* the smallest integer identifier will be returned.
* <p>
* By default, the provider reports {@link #DEFAULT_DEVICE_STATE} when no configuration file is
* provided.
*/
public final class DeviceStateProviderImpl implements DeviceStateProvider,
InputManagerInternal.LidSwitchCallback, SensorEventListener {
private static final String TAG = "DeviceStateProviderImpl";
private static final BooleanSupplier TRUE_BOOLEAN_SUPPLIER = () -> true;
private static final BooleanSupplier FALSE_BOOLEAN_SUPPLIER = () -> false;
@VisibleForTesting
static final DeviceState DEFAULT_DEVICE_STATE = new DeviceState(MINIMUM_DEVICE_STATE,
"DEFAULT");
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) {
List<DeviceState> deviceStateList = new ArrayList<>();
List<Conditions> conditionsList = new ArrayList<>();
if (readableConfig != null) {
DeviceStateConfig config = parseConfig(readableConfig);
if (config != null) {
for (com.android.server.policy.devicestate.config.DeviceState stateConfig :
config.getDeviceState()) {
final int state = stateConfig.getIdentifier().intValue();
final String name = stateConfig.getName() == null ? "" : stateConfig.getName();
deviceStateList.add(new DeviceState(state, name));
final Conditions condition = stateConfig.getConditions();
conditionsList.add(condition);
}
}
}
if (deviceStateList.size() == 0) {
deviceStateList.add(DEFAULT_DEVICE_STATE);
conditionsList.add(null);
}
return new DeviceStateProviderImpl(context, deviceStateList, conditionsList);
}
// Lock for internal state.
private final Object mLock = new Object();
private final Context mContext;
// List of supported states in ascending order based on their identifier.
private final DeviceState[] mOrderedStates;
// Map of state identifier 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 = new SparseArray<>();
@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 List<DeviceState> deviceStates,
@NonNull List<Conditions> stateConditions) {
Preconditions.checkArgument(deviceStates.size() == stateConditions.size(),
"Number of device states must be equal to the number of device state conditions.");
mContext = context;
DeviceState[] orderedStates = deviceStates.toArray(new DeviceState[deviceStates.size()]);
Arrays.sort(orderedStates, Comparator.comparingInt(DeviceState::getIdentifier));
mOrderedStates = orderedStates;
setStateConditions(deviceStates, stateConditions);
}
private void setStateConditions(@NonNull List<DeviceState> deviceStates,
@NonNull List<Conditions> stateConditions) {
// 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;
// The set of Sensor(s) that this instance should register to receive SensorEvent(s) from.
final ArraySet<Sensor> sensorsToListenTo = new ArraySet<>();
for (int i = 0; i < stateConditions.size(); i++) {
final int state = deviceStates.get(i).getIdentifier();
final Conditions conditions = stateConditions.get(i);
if (conditions == null) {
mStateConditions.put(state, TRUE_BOOLEAN_SUPPLIER);
continue;
}
// Whether or not all the required hardware components could be found that match the
// requirements from the config.
boolean allRequiredComponentsFound = true;
// Whether or not this condition requires the lid switch.
boolean lidSwitchRequired = false;
// Set of sensors required for this condition.
ArraySet<Sensor> sensorsRequired = new ArraySet<>();
List<BooleanSupplier> suppliers = new ArrayList<>();
LidSwitchCondition lidSwitchCondition = conditions.getLidSwitch();
if (lidSwitchCondition != null) {
suppliers.add(new LidSwitchBooleanSupplier(lidSwitchCondition.getOpen()));
lidSwitchRequired = true;
}
List<SensorCondition> sensorConditions = conditions.getSensor();
for (int j = 0; j < sensorConditions.size(); j++) {
SensorCondition sensorCondition = sensorConditions.get(j);
final String expectedSensorType = sensorCondition.getType();
final String expectedSensorName = sensorCondition.getName();
final Sensor foundSensor = findSensor(expectedSensorType, expectedSensorName);
if (foundSensor == null) {
Slog.e(TAG, "Failed to find Sensor with type: " + expectedSensorType
+ " and name: " + expectedSensorName);
allRequiredComponentsFound = false;
break;
}
suppliers.add(new SensorBooleanSupplier(foundSensor, sensorCondition.getValue()));
sensorsRequired.add(foundSensor);
}
if (allRequiredComponentsFound) {
shouldListenToLidSwitch |= lidSwitchRequired;
sensorsToListenTo.addAll(sensorsRequired);
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);
}
} else {
// Failed to setup this condition. This can happen if a sensor is missing. Default
// this state to always false.
mStateConditions.put(state, FALSE_BOOLEAN_SUPPLIER);
}
}
if (shouldListenToLidSwitch) {
InputManagerInternal inputManager = LocalServices.getService(
InputManagerInternal.class);
inputManager.registerLidSwitchCallback(this);
}
final SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
for (int i = 0; i < sensorsToListenTo.size(); i++) {
Sensor sensor = sensorsToListenTo.valueAt(i);
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}
}
@Nullable
private Sensor findSensor(String type, String name) {
final SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
final List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
for (int sensorIndex = 0; sensorIndex < sensors.size(); sensorIndex++) {
final Sensor sensor = sensors.get(sensorIndex);
final String sensorType = sensor.getStringType();
final String sensorName = sensor.getName();
if (sensorType == null || sensorName == null) {
continue;
}
if (sensorType.equals(type) && sensorName.equals(name)) {
return sensor;
}
}
return null;
}
@Override
public void setListener(Listener listener) {
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() {
DeviceState[] supportedStates;
synchronized (mLock) {
if (mListener == null) {
return;
}
supportedStates = Arrays.copyOf(mOrderedStates, mOrderedStates.length);
}
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].getIdentifier();
for (int i = 0; i < mOrderedStates.length; i++) {
int state = mOrderedStates[i].getIdentifier();
boolean conditionSatisfied;
try {
conditionSatisfied = mStateConditions.get(state).getAsBoolean();
} catch (IllegalStateException e) {
// Failed to compute the current state based on current available data. Return
// with the expectation that notifyDeviceStateChangedIfNeeded() will be called
// when a callback with the missing data is triggered.
return;
}
if (conditionSatisfied) {
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) {
if (mIsLidOpen == null) {
throw new IllegalStateException("Have not received lid switch value.");
}
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) {
throw new IllegalStateException("Have not received sensor event.");
}
if (latestEvent.values.length < mExpectedValues.size()) {
throw new RuntimeException("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 < mExpectedValues.size(); 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);
}
}
}