Merge "Add DeviceStateManagerService and stub implementation of policy."

This commit is contained in:
Darryl Johnson
2020-10-08 16:52:04 +00:00
committed by Android (Google) Code Review
8 changed files with 700 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
/*
* 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);
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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);
}

View File

@@ -0,0 +1,69 @@
/*
* 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);
}
}

View File

@@ -0,0 +1,3 @@
ogunwale@google.com
akulian@google.com
darryljohnson@google.com

View File

@@ -0,0 +1,44 @@
/*
* 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();
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.Nullable;
import com.android.server.devicestate.DeviceStateProvider;
/**
* Default implementation of {@link DeviceStateProvider}. Currently only supports
* {@link #DEFAULT_DEVICE_STATE}.
*
* @see DeviceStatePolicyImpl
*/
final class DeviceStateProviderImpl implements DeviceStateProvider {
private static final int DEFAULT_DEVICE_STATE = 0;
@Nullable
private Listener mListener = null;
@Override
public void setListener(Listener listener) {
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);
}
}

View File

@@ -107,6 +107,7 @@ import com.android.server.connectivity.IpConnectivityMetrics;
import com.android.server.contentcapture.ContentCaptureManagerInternal;
import com.android.server.coverage.CoverageService;
import com.android.server.devicepolicy.DevicePolicyManagerService;
import com.android.server.devicestate.DeviceStateManagerService;
import com.android.server.display.DisplayManagerService;
import com.android.server.display.color.ColorDisplayService;
import com.android.server.dreams.DreamManagerService;
@@ -1254,6 +1255,10 @@ public final class SystemServer {
mSystemServiceManager.startService(AppIntegrityManagerService.class);
t.traceEnd();
t.traceBegin("DeviceStateManagerService");
mSystemServiceManager.startService(DeviceStateManagerService.class);
t.traceEnd();
} catch (Throwable e) {
Slog.e("System", "******************************************");
Slog.e("System", "************ Failure starting core service");

View File

@@ -0,0 +1,224 @@
/*
* 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 static com.android.server.devicestate.DeviceStateManagerService.INVALID_DEVICE_STATE;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertThrows;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Unit tests for {@link DeviceStateManagerService}.
* <p/>
* Run with <code>atest DeviceStateManagerServiceTest</code>.
*/
@RunWith(AndroidJUnit4.class)
public final class DeviceStateManagerServiceTest {
private static final int DEFAULT_DEVICE_STATE = 0;
private static final int OTHER_DEVICE_STATE = 1;
private static final int UNSUPPORTED_DEVICE_STATE = 999;
private TestDeviceStatePolicy mPolicy;
private TestDeviceStateProvider mProvider;
private DeviceStateManagerService mService;
@Before
public void setup() {
mProvider = new TestDeviceStateProvider();
mPolicy = new TestDeviceStatePolicy(mProvider);
mService = new DeviceStateManagerService(InstrumentationRegistry.getContext(), mPolicy);
mService.onStart();
}
@Test
public void requestStateChange() {
assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
assertEquals(mService.getPendingState(), INVALID_DEVICE_STATE);
assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE);
mProvider.notifyRequestState(OTHER_DEVICE_STATE);
assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE);
assertEquals(mService.getPendingState(), INVALID_DEVICE_STATE);
assertEquals(mService.getRequestedState(), OTHER_DEVICE_STATE);
assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), OTHER_DEVICE_STATE);
}
@Test
public void requestStateChange_pendingState() {
mPolicy.blockConfigure();
mProvider.notifyRequestState(OTHER_DEVICE_STATE);
assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
assertEquals(mService.getPendingState(), OTHER_DEVICE_STATE);
assertEquals(mService.getRequestedState(), OTHER_DEVICE_STATE);
assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), OTHER_DEVICE_STATE);
mProvider.notifyRequestState(DEFAULT_DEVICE_STATE);
assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
assertEquals(mService.getPendingState(), OTHER_DEVICE_STATE);
assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), OTHER_DEVICE_STATE);
mPolicy.resumeConfigure();
assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
assertEquals(mService.getPendingState(), INVALID_DEVICE_STATE);
assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE);
}
@Test
public void requestStateChange_unsupportedState() {
mProvider.notifyRequestState(UNSUPPORTED_DEVICE_STATE);
assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
assertEquals(mService.getPendingState(), INVALID_DEVICE_STATE);
assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE);
}
@Test
public void requestStateChange_invalidState() {
assertThrows(IllegalArgumentException.class, () -> {
mProvider.notifyRequestState(INVALID_DEVICE_STATE);
});
}
@Test
public void supportedStatesChanged() {
assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
assertEquals(mService.getPendingState(), INVALID_DEVICE_STATE);
assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
mProvider.notifySupportedDeviceStates(new int []{ DEFAULT_DEVICE_STATE });
// The current committed and requests states do not change because the current state remains
// supported.
assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
assertEquals(mService.getPendingState(), INVALID_DEVICE_STATE);
assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
}
@Test
public void supportedStatesChanged_invalidState() {
assertThrows(IllegalArgumentException.class, () -> {
mProvider.notifySupportedDeviceStates(new int []{ INVALID_DEVICE_STATE });
});
}
@Test
public void supportedStatesChanged_unsupportedRequestedState() {
assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
assertEquals(mService.getPendingState(), INVALID_DEVICE_STATE);
assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
mProvider.notifySupportedDeviceStates(new int []{ OTHER_DEVICE_STATE });
// The current requested state is cleared because it is no longer supported.
assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
assertEquals(mService.getPendingState(), INVALID_DEVICE_STATE);
assertEquals(mService.getRequestedState(), INVALID_DEVICE_STATE);
mProvider.notifyRequestState(OTHER_DEVICE_STATE);
assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE);
assertEquals(mService.getPendingState(), INVALID_DEVICE_STATE);
assertEquals(mService.getRequestedState(), OTHER_DEVICE_STATE);
}
private static final class TestDeviceStatePolicy implements DeviceStatePolicy {
private final DeviceStateProvider mProvider;
private int mLastDeviceStateRequestedToConfigure = INVALID_DEVICE_STATE;
private boolean mConfigureBlocked = false;
private Runnable mPendingConfigureCompleteRunnable;
TestDeviceStatePolicy(DeviceStateProvider provider) {
mProvider = provider;
}
@Override
public DeviceStateProvider getDeviceStateProvider() {
return mProvider;
}
public void blockConfigure() {
mConfigureBlocked = true;
}
public void resumeConfigure() {
mConfigureBlocked = false;
if (mPendingConfigureCompleteRunnable != null) {
Runnable onComplete = mPendingConfigureCompleteRunnable;
mPendingConfigureCompleteRunnable = null;
onComplete.run();
}
}
public int getMostRecentRequestedStateToConfigure() {
return mLastDeviceStateRequestedToConfigure;
}
@Override
public void configureDeviceForState(int state, Runnable onComplete) {
if (mPendingConfigureCompleteRunnable != null) {
throw new IllegalStateException("configureDeviceForState() called while configure"
+ " is pending");
}
mLastDeviceStateRequestedToConfigure = state;
if (mConfigureBlocked) {
mPendingConfigureCompleteRunnable = onComplete;
return;
}
onComplete.run();
}
}
private static final class TestDeviceStateProvider implements DeviceStateProvider {
private int[] mSupportedDeviceStates = new int[]{ DEFAULT_DEVICE_STATE,
OTHER_DEVICE_STATE };
private int mCurrentDeviceState = DEFAULT_DEVICE_STATE;
private Listener mListener;
@Override
public void setListener(Listener listener) {
if (mListener != null) {
throw new IllegalArgumentException("Provider already has listener set.");
}
mListener = listener;
mListener.onSupportedDeviceStatesChanged(mSupportedDeviceStates);
mListener.onStateChanged(mCurrentDeviceState);
}
public void notifySupportedDeviceStates(int[] supportedDeviceStates) {
mSupportedDeviceStates = supportedDeviceStates;
mListener.onSupportedDeviceStatesChanged(supportedDeviceStates);
}
public void notifyRequestState(int state) {
mCurrentDeviceState = state;
mListener.onStateChanged(state);
}
}
}