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

Commit b1677542 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Darryl L Johnson
Browse files

Add DeviceStateManagerService and stub implementation of policy.

Test: atest DeviceStateManagerServiceTest
Bug: 159401801

Change-Id: Ib7f424e905c80812b36e0e0bb30771224ce56e34
parent e9988880
Loading
Loading
Loading
Loading
+270 −0
Original line number Diff line number Diff line
/*
 * 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.devicestate;

import android.annotation.NonNull;
import android.content.Context;
import android.util.IntArray;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemService;
import com.android.server.policy.DeviceStatePolicyImpl;

import java.util.Arrays;

/**
 * A system service that manages the state of a device with user-configurable hardware like a
 * foldable phone.
 * <p>
 * Device state is an abstract concept that allows mapping the current state of the device to the
 * state of the system. For example, system services (like
 * {@link com.android.server.display.DisplayManagerService display manager} and
 * {@link com.android.server.wm.WindowManagerService window manager}) and system UI may have
 * different behaviors depending on the physical state of the device. This is useful for
 * variable-state devices, like foldable or rollable devices, that can be configured by users into
 * differing hardware states, which each may have a different expected use case.
 * </p>
 * <p>
 * The {@link DeviceStateManagerService} is responsible for receiving state change requests from
 * the {@link DeviceStateProvider} to modify the current device state and communicating with the
 * {@link DeviceStatePolicy policy} to ensure the system is configured to match the requested state.
 * </p>
 *
 * @see DeviceStatePolicy
 */
public final class DeviceStateManagerService extends SystemService {
    /** Invalid device state. */
    public static final int INVALID_DEVICE_STATE = -1;

    private static final String TAG = "DeviceStateManagerService";
    private static final boolean DEBUG = false;

    private final Object mLock = new Object();
    @NonNull
    private final DeviceStatePolicy mDeviceStatePolicy;

    @GuardedBy("mLock")
    private IntArray mSupportedDeviceStates;

    // The current committed device state.
    @GuardedBy("mLock")
    private int mCommittedState = INVALID_DEVICE_STATE;
    // The device state that is currently pending callback from the policy to be committed.
    @GuardedBy("mLock")
    private int mPendingState = INVALID_DEVICE_STATE;
    // Whether or not the policy is currently waiting to be notified of the current pending state.
    @GuardedBy("mLock")
    private boolean mIsPolicyWaitingForState = false;
    // The device state that is currently requested and is next to be configured and committed.
    @GuardedBy("mLock")
    private int mRequestedState = INVALID_DEVICE_STATE;

    public DeviceStateManagerService(@NonNull Context context) {
        this(context, new DeviceStatePolicyImpl());
    }

    @VisibleForTesting
    DeviceStateManagerService(@NonNull Context context, @NonNull DeviceStatePolicy policy) {
        super(context);
        mDeviceStatePolicy = policy;
    }

    @Override
    public void onStart() {
        mDeviceStatePolicy.getDeviceStateProvider().setListener(new DeviceStateProviderListener());
    }

    /**
     * Returns the current state the system is in. Note that the system may be in the process of
     * configuring a different state.
     *
     * @see #getPendingState()
     */
    @VisibleForTesting
    int getCommittedState() {
        synchronized (mLock) {
            return mCommittedState;
        }
    }

    /**
     * Returns the state the system is currently configuring, or {@link #INVALID_DEVICE_STATE} if
     * the system is not in the process of configuring a state.
     */
    @VisibleForTesting
    int getPendingState() {
        synchronized (mLock) {
            return mPendingState;
        }
    }

    /**
     * Returns the requested state. The service will configure the device to match the requested
     * state when possible.
     */
    @VisibleForTesting
    int getRequestedState() {
        synchronized (mLock) {
            return mRequestedState;
        }
    }

    private void updateSupportedStates(int[] supportedDeviceStates) {
        // Must ensure sorted as isSupportedStateLocked() impl uses binary search.
        Arrays.sort(supportedDeviceStates, 0, supportedDeviceStates.length);
        synchronized (mLock) {
            mSupportedDeviceStates = IntArray.wrap(supportedDeviceStates);

            if (mRequestedState != INVALID_DEVICE_STATE
                    && !isSupportedStateLocked(mRequestedState)) {
                // The current requested state is no longer valid. We'll clear it here, though
                // we won't actually update the current state with a call to
                // updatePendingStateLocked() as doing so will not have any effect.
                mRequestedState = INVALID_DEVICE_STATE;
            }
        }
    }

    /**
     * Returns {@code true} if the provided state is supported. Requires that
     * {@link #mSupportedDeviceStates} is sorted prior to calling.
     */
    private boolean isSupportedStateLocked(int state) {
        return mSupportedDeviceStates.binarySearch(state) >= 0;
    }

    /**
     * Requests that the system enter the provided {@code state}. The request may not be honored
     * under certain conditions, for example if the provided state is not supported.
     *
     * @see #isSupportedStateLocked(int)
     */
    private void requestState(int state) {
        synchronized (mLock) {
            if (isSupportedStateLocked(state)) {
                mRequestedState = state;
            }
            updatePendingStateLocked();
        }

        notifyPolicyIfNeeded();
    }

    /**
     * Tries to update the current configuring state with the current requested state. Must call
     * {@link #notifyPolicyIfNeeded()} to actually notify the policy that the state is being
     * changed.
     */
    private void updatePendingStateLocked() {
        if (mRequestedState == INVALID_DEVICE_STATE) {
            // No currently requested state.
            return;
        }

        if (mPendingState != INVALID_DEVICE_STATE) {
            // Have pending state, can not configure a new state until the state is committed.
            return;
        }

        if (mRequestedState == mCommittedState) {
            // No need to notify the policy as the committed state matches the requested state.
            return;
        }

        mPendingState = mRequestedState;
        mIsPolicyWaitingForState = true;
    }

    /**
     * Notifies the policy to configure the supplied state. Should not be called with {@link #mLock}
     * held.
     */
    private void notifyPolicyIfNeeded() {
        if (Thread.holdsLock(mLock)) {
            Throwable error = new Throwable("Attempting to notify DeviceStatePolicy with service"
                    + " lock held");
            error.fillInStackTrace();
            Slog.w(TAG, error);
        }
        int state;
        synchronized (mLock) {
            if (!mIsPolicyWaitingForState) {
                return;
            }
            mIsPolicyWaitingForState = false;
            state = mPendingState;
        }

        if (DEBUG) {
            Slog.d(TAG, "Notifying policy to configure state: " + state);
        }
        mDeviceStatePolicy.configureDeviceForState(state, this::commitPendingState);
    }

    /**
     * Commits the current pending state after a callback from the {@link DeviceStatePolicy}.
     *
     * <pre>
     *              -------------    -----------              -------------
     * Provider ->  | Requested | -> | Pending | -> Policy -> | Committed |
     *              -------------    -----------              -------------
     * </pre>
     * <p>
     * When a new state is requested it immediately enters the requested state. Once the policy is
     * available to accept a new state, which could also be immediately if there is no current
     * pending state at the point of request, the policy is notified and a callback is provided to
     * trigger the state to be committed.
     * </p>
     */
    private void commitPendingState() {
        synchronized (mLock) {
            if (DEBUG) {
                Slog.d(TAG, "Committing state: " + mPendingState);
            }
            mCommittedState = mPendingState;
            mPendingState = INVALID_DEVICE_STATE;
            updatePendingStateLocked();
        }

        notifyPolicyIfNeeded();
    }

    private final class DeviceStateProviderListener implements DeviceStateProvider.Listener {
        @Override
        public void onSupportedDeviceStatesChanged(int[] newDeviceStates) {
            for (int i = 0; i < newDeviceStates.length; i++) {
                if (newDeviceStates[i] < 0) {
                    throw new IllegalArgumentException("Supported device states includes invalid"
                            + " value: " + newDeviceStates[i]);
                }
            }

            updateSupportedStates(newDeviceStates);
        }

        @Override
        public void onStateChanged(int state) {
            if (state < 0) {
                throw new IllegalArgumentException("Invalid state: " + state);
            }

            requestState(state);
        }
    }
}
+40 −0
Original line number Diff line number Diff line
/*
 * 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.devicestate;

import android.annotation.NonNull;

/**
 * Interface for the component responsible for supplying the current device state as well as
 * configuring the state of the system in response to device state changes.
 *
 * @see DeviceStateManagerService
 */
public interface DeviceStatePolicy {
    /** Returns the provider of device states. */
    DeviceStateProvider getDeviceStateProvider();

    /**
     * Configures the system into the provided state. Guaranteed not to be called again until the
     * {@code onComplete} callback has been executed.
     *
     * @param state the state the system should be configured for.
     * @param onComplete a callback that must be triggered once the system has been properly
     *                   configured to match the supplied state.
     */
    void configureDeviceForState(int state, @NonNull Runnable onComplete);
}
+69 −0
Original line number Diff line number Diff line
/*
 * 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.devicestate;

/**
 * Responsible for providing the set of currently supported device states and well as the current
 * device state.
 *
 * @see DeviceStatePolicy
 */
public interface DeviceStateProvider {
    /**
     * Registers a listener for changes in provider state.
     * <p>
     * It is <b>required</b> that {@link Listener#onSupportedDeviceStatesChanged(int[])} be called
     * followed by {@link Listener#onStateChanged(int)} with the initial values on successful
     * registration of the listener.
     */
    void setListener(Listener listener);

    /** Callback for changes in {@link DeviceStateProvider} state. */
    interface Listener {
        /**
         * Called to notify the listener of a change in supported device states. Required to be
         * called once on successful registration of the listener and then once on every
         * subsequent change in supported device states.
         * <p>
         * The set of device states can change based on the current hardware state of the device.
         * For example, if a device state depends on a particular peripheral device (display, etc)
         * it would only be reported as supported when the device is plugged. Otherwise, it should
         * not be included in the set of supported states.
         * <p>
         * All values provided must be greater than or equal to zero and there must always be at
         * least one supported device state.
         *
         * @param newDeviceStates array of supported device states.
         *
         * @throws IllegalArgumentException if the list of device states is empty or if one of the
         * provided states is less than 0.
         */
        void onSupportedDeviceStatesChanged(int[] newDeviceStates);

        /**
         * Called to notify the listener of a change in current device state. Required to be called
         * once on successful registration of the listener and then once on every subsequent change
         * in device state. Value must have been included in the set of supported device states
         * provided in the most recent call to {@link #onSupportedDeviceStatesChanged(int[])}.
         *
         * @param state the new device state.
         *
         * @throws IllegalArgumentException if the state is less than 0.
         */
        void onStateChanged(int state);
    }
}
+3 −0
Original line number Diff line number Diff line
ogunwale@google.com
akulian@google.com
darryljohnson@google.com
+44 −0
Original line number Diff line number Diff line
/*
 * 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 android.annotation.NonNull;

import com.android.server.devicestate.DeviceStatePolicy;
import com.android.server.devicestate.DeviceStateProvider;

/**
 * Default empty implementation of {@link DeviceStatePolicy}.
 *
 * @see DeviceStateProviderImpl
 */
public final class DeviceStatePolicyImpl implements DeviceStatePolicy {
    private final DeviceStateProvider mProvider;

    public DeviceStatePolicyImpl() {
        mProvider = new DeviceStateProviderImpl();
    }

    public DeviceStateProvider getDeviceStateProvider() {
        return mProvider;
    }

    @Override
    public void configureDeviceForState(int state, @NonNull Runnable onComplete) {
        onComplete.run();
    }
}
Loading