Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1ddaa9f6 authored by Darryl L Johnson's avatar Darryl L Johnson
Browse files

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
parent 79477111
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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",
+1 −1
Original line number Diff line number Diff line
@@ -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() {
+266 −11
Original line number Diff line number Diff line
@@ -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}.
 * 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.
 */
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.
     *
 * @see DeviceStatePolicyImpl
     * @param readableConfig the config the provider instance should read supported states from.
     */
final class DeviceStateProviderImpl implements DeviceStateProvider {
    private static final int DEFAULT_DEVICE_STATE = 0;
    @VisibleForTesting
    static DeviceStateProviderImpl createFromConfig(@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(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<BooleanSupplier> mStateConditions;

    @Nullable
    @GuardedBy("mLock")
    private Listener mListener = null;
    @GuardedBy("mLock")
    private int mLastReportedState = INVALID_DEVICE_STATE;

    @GuardedBy("mLock")
    private boolean mIsLidOpen;

    private DeviceStateProviderImpl(SparseArray<Conditions> 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) {
        synchronized (mLock) {
            if (mListener != null) {
                throw new RuntimeException("Provider already has a listener set.");
            }

            mListener = listener;
        mListener.onSupportedDeviceStatesChanged(new int[]{ DEFAULT_DEVICE_STATE });
        mListener.onStateChanged(DEFAULT_DEVICE_STATE);
        }
        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.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.
     * <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);
        }
    }
}
+7 −0
Original line number Diff line number Diff line
@@ -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",
}
+57 −0
Original line number Diff line number Diff line
<?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:sequence>
    </xs:complexType>

    <xs:complexType name="lidSwitchCondition">
        <xs:sequence>
            <xs:element name="open" type="xs:boolean" />
        </xs:sequence>
    </xs:complexType>

</xs:schema>
Loading