From c26defea8f5c6c0244888ab586a8b702ad701c91 Mon Sep 17 00:00:00 2001 From: Darryl L Johnson Date: Tue, 2 Feb 2021 16:08:53 -0800 Subject: [PATCH] Add device state request APIs to DeviceStateManager. This change promotes DeviceStateManager to a TestApi and exposes three additional methods: - getSupportedStates: returns a list of state ids that can be used with requestState(). - submitRequest: requests that the device enter the supplied state. - cancelRequest: clears the current requested device state. to be used initially in CTS tests. Bug: 177235528 Bug: 177236115 Test: atest DeviceStateManagerServiceTest Test: atest DeviceStateManagerGlobalTest Change-Id: Ieae9208420ddecea641a5c44f1610d3cb7071b07 --- core/api/test-current.txt | 35 ++ .../devicestate/DeviceStateManager.java | 74 ++- .../devicestate/DeviceStateManagerGlobal.java | 191 ++++++- .../devicestate/DeviceStateRequest.java | 163 ++++++ .../devicestate/IDeviceStateManager.aidl | 40 ++ .../IDeviceStateManagerCallback.aidl | 37 ++ core/res/AndroidManifest.xml | 2 +- .../DeviceStateManagerGlobalTest.java | 155 +++++- .../server/devicestate/DeviceState.java | 10 +- .../DeviceStateManagerService.java | 505 ++++++++++++++---- .../DeviceStateManagerShellCommand.java | 89 ++- .../server/display/DisplayManagerService.java | 4 +- .../server/policy/DisplayFoldController.java | 4 +- .../DeviceStateManagerServiceTest.java | 273 +++++++--- 14 files changed, 1342 insertions(+), 240 deletions(-) create mode 100644 core/java/android/hardware/devicestate/DeviceStateRequest.java diff --git a/core/api/test-current.txt b/core/api/test-current.txt index e0391eee5077d..3bf5abc01fa32 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -12,6 +12,7 @@ package android { field public static final String CLEAR_APP_USER_DATA = "android.permission.CLEAR_APP_USER_DATA"; field public static final String CONFIGURE_DISPLAY_BRIGHTNESS = "android.permission.CONFIGURE_DISPLAY_BRIGHTNESS"; field public static final String CONTROL_DEVICE_LIGHTS = "android.permission.CONTROL_DEVICE_LIGHTS"; + field public static final String CONTROL_DEVICE_STATE = "android.permission.CONTROL_DEVICE_STATE"; field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES"; field @Deprecated public static final String MANAGE_ACTIVITY_STACKS = "android.permission.MANAGE_ACTIVITY_STACKS"; field public static final String MANAGE_ACTIVITY_TASKS = "android.permission.MANAGE_ACTIVITY_TASKS"; @@ -895,6 +896,40 @@ package android.hardware.camera2 { } +package android.hardware.devicestate { + + public final class DeviceStateManager { + method public void addDeviceStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.devicestate.DeviceStateManager.DeviceStateListener); + method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) public void cancelRequest(@NonNull android.hardware.devicestate.DeviceStateRequest); + method @NonNull public int[] getSupportedStates(); + method public void removeDeviceStateListener(@NonNull android.hardware.devicestate.DeviceStateManager.DeviceStateListener); + method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) public void requestState(@NonNull android.hardware.devicestate.DeviceStateRequest, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.devicestate.DeviceStateRequest.Callback); + } + + public static interface DeviceStateManager.DeviceStateListener { + method public void onDeviceStateChanged(int); + } + + public final class DeviceStateRequest { + method public int getFlags(); + method public int getState(); + method @NonNull public static android.hardware.devicestate.DeviceStateRequest.Builder newBuilder(int); + field public static final int FLAG_CANCEL_WHEN_BASE_CHANGES = 1; // 0x1 + } + + public static final class DeviceStateRequest.Builder { + method @NonNull public android.hardware.devicestate.DeviceStateRequest build(); + method @NonNull public android.hardware.devicestate.DeviceStateRequest.Builder setFlags(int); + } + + public static interface DeviceStateRequest.Callback { + method public default void onRequestActivated(@NonNull android.hardware.devicestate.DeviceStateRequest); + method public default void onRequestCanceled(@NonNull android.hardware.devicestate.DeviceStateRequest); + method public default void onRequestSuspended(@NonNull android.hardware.devicestate.DeviceStateRequest); + } + +} + package android.hardware.display { public class AmbientDisplayConfiguration { diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java index 29a6ee278d9c3..f175e7b00b7e2 100644 --- a/core/java/android/hardware/devicestate/DeviceStateManager.java +++ b/core/java/android/hardware/devicestate/DeviceStateManager.java @@ -16,8 +16,12 @@ package android.hardware.devicestate; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SystemService; +import android.annotation.TestApi; import android.content.Context; import java.util.concurrent.Executor; @@ -28,13 +32,19 @@ import java.util.concurrent.Executor; * * @hide */ +@TestApi @SystemService(Context.DEVICE_STATE_SERVICE) public final class DeviceStateManager { - /** Invalid device state. */ + /** + * Invalid device state. + * + * @hide + */ public static final int INVALID_DEVICE_STATE = -1; - private DeviceStateManagerGlobal mGlobal; + private final DeviceStateManagerGlobal mGlobal; + /** @hide */ public DeviceStateManager() { DeviceStateManagerGlobal global = DeviceStateManagerGlobal.getInstance(); if (global == null) { @@ -44,24 +54,74 @@ public final class DeviceStateManager { mGlobal = global; } + /** + * Returns the list of device states that are supported and can be requested with + * {@link #requestState(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}. + */ + @NonNull + public int[] getSupportedStates() { + return mGlobal.getSupportedStates(); + } + + /** + * Submits a {@link DeviceStateRequest request} to modify the device state. + *

+ * By default, the request is kept active until a call to + * {@link #cancelRequest(DeviceStateRequest)} or until one of the following occurs: + *

+ * However, this behavior can be changed by setting flags on the {@link DeviceStateRequest}. + * + * @throws IllegalArgumentException if the requested state is unsupported. + * @throws SecurityException if the {@link android.Manifest.permission#CONTROL_DEVICE_STATE} + * permission is not held. + * + * @see DeviceStateRequest + */ + @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) + public void requestState(@NonNull DeviceStateRequest request, + @Nullable @CallbackExecutor Executor executor, + @Nullable DeviceStateRequest.Callback callback) { + mGlobal.requestState(request, callback, executor); + } + + /** + * Cancels a {@link DeviceStateRequest request} previously submitted with a call to + * {@link #requestState(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}. + *

+ * This method is noop if the {@code request} has not been submitted with a call to + * {@link #requestState(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}. + * + * @throws SecurityException if the {@link android.Manifest.permission#CONTROL_DEVICE_STATE} + * permission is not held. + */ + @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) + public void cancelRequest(@NonNull DeviceStateRequest request) { + mGlobal.cancelRequest(request); + } + /** * Registers a listener to receive notifications about changes in device state. * - * @param listener the listener to register. * @param executor the executor to process notifications. + * @param listener the listener to register. * * @see DeviceStateListener */ - public void registerDeviceStateListener(@NonNull DeviceStateListener listener, - @NonNull Executor executor) { + public void addDeviceStateListener(@NonNull @CallbackExecutor Executor executor, + @NonNull DeviceStateListener listener) { mGlobal.registerDeviceStateListener(listener, executor); } /** * Unregisters a listener previously registered with - * {@link #registerDeviceStateListener(DeviceStateListener, Executor)}. + * {@link #addDeviceStateListener(Executor, DeviceStateListener)}. */ - public void unregisterDeviceStateListener(@NonNull DeviceStateListener listener) { + public void removeDeviceStateListener(@NonNull DeviceStateListener listener) { mGlobal.unregisterDeviceStateListener(listener); } diff --git a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java index c8905038d0560..b9ae88ea840f2 100644 --- a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java +++ b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java @@ -20,9 +20,11 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.hardware.devicestate.DeviceStateManager.DeviceStateListener; +import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; +import android.util.ArrayMap; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -67,6 +69,9 @@ public final class DeviceStateManagerGlobal { @GuardedBy("mLock") private final ArrayList mListeners = new ArrayList<>(); + @GuardedBy("mLock") + private final ArrayMap mRequests = new ArrayMap<>(); + @Nullable @GuardedBy("mLock") private Integer mLastReceivedState; @@ -76,10 +81,85 @@ public final class DeviceStateManagerGlobal { mDeviceStateManager = deviceStateManager; } + /** + * Returns the set of supported device states. + * + * @see DeviceStateManager#getSupportedStates() + */ + public int[] getSupportedStates() { + try { + return mDeviceStateManager.getSupportedDeviceStates(); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + /** + * Submits a {@link DeviceStateRequest request} to modify the device state. + * + * @see DeviceStateManager#requestState(DeviceStateRequest, + * Executor, DeviceStateRequest.Callback) + * @see DeviceStateRequest + */ + public void requestState(@NonNull DeviceStateRequest request, + @Nullable DeviceStateRequest.Callback callback, @Nullable Executor executor) { + if (callback == null && executor != null) { + throw new IllegalArgumentException("Callback must be supplied with executor."); + } else if (executor == null && callback != null) { + throw new IllegalArgumentException("Executor must be supplied with callback."); + } + + synchronized (mLock) { + registerCallbackIfNeededLocked(); + + if (findRequestTokenLocked(request) != null) { + // This request has already been submitted. + return; + } + + // Add the request wrapper to the mRequests array before requesting the state as the + // callback could be triggered immediately if the mDeviceStateManager IBinder is in the + // same process as this instance. + IBinder token = new Binder(); + mRequests.put(token, new DeviceStateRequestWrapper(request, callback, executor)); + + try { + mDeviceStateManager.requestState(token, request.getState(), request.getFlags()); + } catch (RemoteException ex) { + mRequests.remove(token); + throw ex.rethrowFromSystemServer(); + } + } + } + + /** + * Cancels a {@link DeviceStateRequest request} previously submitted with a call to + * {@link #requestState(DeviceStateRequest, DeviceStateRequest.Callback, Executor)}. + * + * @see DeviceStateManager#cancelRequest(DeviceStateRequest) + */ + public void cancelRequest(@NonNull DeviceStateRequest request) { + synchronized (mLock) { + registerCallbackIfNeededLocked(); + + final IBinder token = findRequestTokenLocked(request); + if (token == null) { + // This request has not been submitted. + return; + } + + try { + mDeviceStateManager.cancelRequest(token); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + } + /** * Registers a listener to receive notifications about changes in device state. * - * @see DeviceStateManager#registerDeviceStateListener(DeviceStateListener, Executor) + * @see DeviceStateManager#addDeviceStateListener(Executor, DeviceStateListener) */ @VisibleForTesting(visibility = Visibility.PACKAGE) public void registerDeviceStateListener(@NonNull DeviceStateListener listener, @@ -112,7 +192,7 @@ public final class DeviceStateManagerGlobal { * Unregisters a listener previously registered with * {@link #registerDeviceStateListener(DeviceStateListener, Executor)}. * - * @see DeviceStateManager#registerDeviceStateListener(DeviceStateListener, Executor) + * @see DeviceStateManager#addDeviceStateListener(Executor, DeviceStateListener) */ @VisibleForTesting(visibility = Visibility.PACKAGE) public void unregisterDeviceStateListener(DeviceStateListener listener) { @@ -144,6 +224,17 @@ public final class DeviceStateManagerGlobal { return -1; } + @Nullable + private IBinder findRequestTokenLocked(@NonNull DeviceStateRequest request) { + for (int i = 0; i < mRequests.size(); i++) { + if (mRequests.valueAt(i).mRequest.equals(request)) { + return mRequests.keyAt(i); + } + } + return null; + } + + /** Handles a call from the server that the device state has changed. */ private void handleDeviceStateChanged(int newDeviceState) { ArrayList listeners; synchronized (mLock) { @@ -156,11 +247,68 @@ public final class DeviceStateManagerGlobal { } } + /** + * Handles a call from the server that a request for the supplied {@code token} has become + * active. + */ + private void handleRequestActive(IBinder token) { + DeviceStateRequestWrapper request; + synchronized (mLock) { + request = mRequests.get(token); + } + if (request != null) { + request.notifyRequestActive(); + } + } + + /** + * Handles a call from the server that a request for the supplied {@code token} has become + * suspended. + */ + private void handleRequestSuspended(IBinder token) { + DeviceStateRequestWrapper request; + synchronized (mLock) { + request = mRequests.get(token); + } + if (request != null) { + request.notifyRequestSuspended(); + } + } + + /** + * Handles a call from the server that a request for the supplied {@code token} has become + * canceled. + */ + private void handleRequestCanceled(IBinder token) { + DeviceStateRequestWrapper request; + synchronized (mLock) { + request = mRequests.remove(token); + } + if (request != null) { + request.notifyRequestCanceled(); + } + } + private final class DeviceStateManagerCallback extends IDeviceStateManagerCallback.Stub { @Override public void onDeviceStateChanged(int deviceState) { handleDeviceStateChanged(deviceState); } + + @Override + public void onRequestActive(IBinder token) { + handleRequestActive(token); + } + + @Override + public void onRequestSuspended(IBinder token) { + handleRequestSuspended(token); + } + + @Override + public void onRequestCanceled(IBinder token) { + handleRequestCanceled(token); + } } private static final class DeviceStateListenerWrapper { @@ -176,4 +324,43 @@ public final class DeviceStateManagerGlobal { mExecutor.execute(() -> mDeviceStateListener.onDeviceStateChanged(newDeviceState)); } } + + private static final class DeviceStateRequestWrapper { + private final DeviceStateRequest mRequest; + @Nullable + private final DeviceStateRequest.Callback mCallback; + @Nullable + private final Executor mExecutor; + + DeviceStateRequestWrapper(@NonNull DeviceStateRequest request, + @Nullable DeviceStateRequest.Callback callback, @Nullable Executor executor) { + mRequest = request; + mCallback = callback; + mExecutor = executor; + } + + void notifyRequestActive() { + if (mCallback == null) { + return; + } + + mExecutor.execute(() -> mCallback.onRequestActivated(mRequest)); + } + + void notifyRequestSuspended() { + if (mCallback == null) { + return; + } + + mExecutor.execute(() -> mCallback.onRequestSuspended(mRequest)); + } + + void notifyRequestCanceled() { + if (mCallback == null) { + return; + } + + mExecutor.execute(() -> mCallback.onRequestSuspended(mRequest)); + } + } } diff --git a/core/java/android/hardware/devicestate/DeviceStateRequest.java b/core/java/android/hardware/devicestate/DeviceStateRequest.java new file mode 100644 index 0000000000000..70f7002597ed4 --- /dev/null +++ b/core/java/android/hardware/devicestate/DeviceStateRequest.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2021 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 android.hardware.devicestate; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.TestApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; + +/** + * A request to alter the state of the device managed by {@link DeviceStateManager}. + *

+ * Once constructed, a {@link DeviceStateRequest request} can be submitted with a call to + * {@link DeviceStateManager#requestState(DeviceStateRequest, Executor, + * DeviceStateRequest.Callback)}. + *

+ * By default, the request is kept active until a call to + * {@link DeviceStateManager#cancelRequest(DeviceStateRequest)} or until one of the following + * occurs: + *

+ * However, this behavior can be changed by setting flags on the request. For example, the + * {@link #FLAG_CANCEL_WHEN_BASE_CHANGES} flag will extend this behavior to also cancel the + * request whenever the base (non-override) device state changes. + * + * @see DeviceStateManager + * + * @hide + */ +@TestApi +public final class DeviceStateRequest { + /** + * Flag that indicates the request should be canceled automatically when the base + * (non-override) device state changes. Useful when the requestor only wants the request to + * remain active while the base state remains constant and automatically cancel when the user + * manipulates the device into a different state. + */ + public static final int FLAG_CANCEL_WHEN_BASE_CHANGES = 1 << 0; + + /** @hide */ + @IntDef(prefix = {"FLAG_"}, flag = true, value = { + FLAG_CANCEL_WHEN_BASE_CHANGES, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RequestFlags {} + + /** + * Creates a new {@link Builder} for a {@link DeviceStateRequest}. Must be one of the supported + * states for the device which can be queried with a call to + * {@link DeviceStateManager#getSupportedStates()}. + * + * @param requestedState the device state being requested. + */ + @NonNull + public static Builder newBuilder(int requestedState) { + return new Builder(requestedState); + } + + /** + * Builder for {@link DeviceStateRequest}. An instance can be obtained through + * {@link #newBuilder(int)}. + */ + public static final class Builder { + private final int mRequestedState; + private int mFlags; + + private Builder(int requestedState) { + mRequestedState = requestedState; + } + + /** + * Sets the flag bits provided within {@code flags} with all other bits remaining + * unchanged. + */ + @NonNull + public Builder setFlags(@RequestFlags int flags) { + mFlags |= flags; + return this; + } + + /** + * Returns a new {@link DeviceStateRequest} object whose state matches the state set on the + * builder. + */ + @NonNull + public DeviceStateRequest build() { + return new DeviceStateRequest(mRequestedState, mFlags); + } + } + + /** Callback to track the status of a request. */ + public interface Callback { + /** + * Called to indicate the request has become active and the device state will match the + * requested state. + *

+ * Guaranteed to be called after a call to + * {@link DeviceStateManager.DeviceStateListener#onDeviceStateChanged(int)} with a state + * matching the requested state. + */ + default void onRequestActivated(@NonNull DeviceStateRequest request) {} + + /** + * Called to indicate the request has been temporarily suspended. + *

+ * Guaranteed to be called before a call to + * {@link DeviceStateManager.DeviceStateListener#onDeviceStateChanged(int)}. + */ + default void onRequestSuspended(@NonNull DeviceStateRequest request) {} + + /** + * Called to indicate the request has been canceled. The request can be resubmitted with + * another call to {@link DeviceStateManager#requestState(DeviceStateRequest, Executor, + * DeviceStateRequest.Callback)}. + *

+ * Guaranteed to be called before a call to + * {@link DeviceStateManager.DeviceStateListener#onDeviceStateChanged(int)}. + *

+ * Note: A call to {@link #onRequestSuspended(DeviceStateRequest)} is not guaranteed to + * occur before this method. + */ + default void onRequestCanceled(@NonNull DeviceStateRequest request) {} + } + + private final int mRequestedState; + @RequestFlags + private final int mFlags; + + private DeviceStateRequest(int requestedState, @RequestFlags int flags) { + mRequestedState = requestedState; + mFlags = flags; + } + + public int getState() { + return mRequestedState; + } + + @RequestFlags + public int getFlags() { + return mFlags; + } +} diff --git a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl index a157b3311ca57..323ad21e4884f 100644 --- a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl +++ b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl @@ -20,5 +20,45 @@ import android.hardware.devicestate.IDeviceStateManagerCallback; /** @hide */ interface IDeviceStateManager { + /** + * Registers a callback to receive notifications from the device state manager. Only one + * callback can be registered per-process. + *

+ * As the callback mechanism is used to alert the caller of changes to request status a callback + * MUST be registered before calling {@link #requestState(IBinder, int, int)} or + * {@link #cancelRequest(IBinder)}. Otherwise an exception will be thrown. + * + * @throws SecurityException if a callback is already registered for the calling process. + */ void registerCallback(in IDeviceStateManagerCallback callback); + + /** Returns the array of supported device state identifiers. */ + int[] getSupportedDeviceStates(); + + /** + * Requests that the device enter the supplied {@code state}. A callback MUST have been + * previously registered with {@link #registerCallback(IDeviceStateManagerCallback)} before a + * call to this method. + * + * @param token the request token previously registered with + * {@link #requestState(IBinder, int, int)} + * + * @throws IllegalStateException if a callback has not yet been registered for the calling + * process. + * @throws IllegalStateException if the supplied {@code token} has already been registered. + * @throws IllegalArgumentException if the supplied {@code state} is not supported. + */ + void requestState(IBinder token, int state, int flags); + + /** + * Cancels a request previously submitted with a call to + * {@link #requestState(IBinder, int, int)}. + * + * @param token the request token previously registered with + * {@link #requestState(IBinder, int, int)} + * + * @throws IllegalStateException if the supplied {@code token} has not been previously + * requested. + */ + void cancelRequest(IBinder token); } diff --git a/core/java/android/hardware/devicestate/IDeviceStateManagerCallback.aidl b/core/java/android/hardware/devicestate/IDeviceStateManagerCallback.aidl index d1c581361b621..ee2a071741ef9 100644 --- a/core/java/android/hardware/devicestate/IDeviceStateManagerCallback.aidl +++ b/core/java/android/hardware/devicestate/IDeviceStateManagerCallback.aidl @@ -18,5 +18,42 @@ package android.hardware.devicestate; /** @hide */ interface IDeviceStateManagerCallback { + /** + * Called in response to a change in device state. Guaranteed to be called once with the initial + * value on registration of the callback. + * + * @param deviceState the new state of the device. + */ oneway void onDeviceStateChanged(int deviceState); + + /** + * Called to notify the callback that a request has become active. Guaranteed to be called + * after a subsequent call to {@link #onDeviceStateChanged(int)} if the request becoming active + * resulted in a device state change. + * + * @param token the request token previously registered with + * {@link IDeviceStateManager#requestState(IBinder, int, int)} + */ + oneway void onRequestActive(IBinder token); + + /** + * Called to notify the callback that a request has become suspended. Guaranteed to be called + * before a subsequent call to {@link #onDeviceStateChanged(int)} if the request becoming + * suspended resulted in a device state change. + * + * @param token the request token previously registered with + * {@link IDeviceStateManager#requestState(IBinder, int, int)} + */ + oneway void onRequestSuspended(IBinder token); + + /** + * Called to notify the callback that a request has become canceled. No further callbacks will + * be triggered for this request. Guaranteed to be called before a subsequent call to + * {@link #onDeviceStateChanged(int)} if the request becoming canceled resulted in a device + * state change. + * + * @param token the request token previously registered with + * {@link IDeviceStateManager#requestState(IBinder, int, int)} + */ + oneway void onRequestCanceled(IBinder token); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5442c03555903..7d110f0e1f20b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -5406,7 +5406,7 @@ - mRequests = new ArrayList<>(); + private Set mCallbacks = new HashSet<>(); @Override @@ -112,19 +164,86 @@ public final class DeviceStateManagerGlobalTest { mCallbacks.add(callback); try { - callback.onDeviceStateChanged(mDeviceState); + callback.onDeviceStateChanged(mMergedState); } catch (RemoteException e) { // Do nothing. Should never happen. } } - public void setDeviceState(int deviceState) { - boolean stateChanged = mDeviceState != deviceState; - mDeviceState = deviceState; - if (stateChanged) { + @Override + public int[] getSupportedDeviceStates() { + return new int[] { DEFAULT_DEVICE_STATE, OTHER_DEVICE_STATE }; + } + + @Override + public void requestState(IBinder token, int state, int flags) { + if (!mRequests.isEmpty()) { + final Request topRequest = mRequests.get(mRequests.size() - 1); for (IDeviceStateManagerCallback callback : mCallbacks) { try { - callback.onDeviceStateChanged(mDeviceState); + callback.onRequestSuspended(topRequest.token); + } catch (RemoteException e) { + // Do nothing. Should never happen. + } + } + } + + final Request request = new Request(token, state, flags); + mRequests.add(request); + notifyStateChangedIfNeeded(); + + for (IDeviceStateManagerCallback callback : mCallbacks) { + try { + callback.onRequestActive(token); + } catch (RemoteException e) { + // Do nothing. Should never happen. + } + } + } + + @Override + public void cancelRequest(IBinder token) { + int index = -1; + for (int i = 0; i < mRequests.size(); i++) { + if (mRequests.get(i).token.equals(token)) { + index = i; + break; + } + } + + if (index == -1) { + throw new IllegalArgumentException("Unknown request: " + token); + } + + mRequests.remove(index); + for (IDeviceStateManagerCallback callback : mCallbacks) { + try { + callback.onRequestCanceled(token); + } catch (RemoteException e) { + // Do nothing. Should never happen. + } + } + notifyStateChangedIfNeeded(); + } + + public void setBaseState(int state) { + mBaseState = state; + notifyStateChangedIfNeeded(); + } + + private void notifyStateChangedIfNeeded() { + final int originalMergedState = mMergedState; + + if (!mRequests.isEmpty()) { + mMergedState = mRequests.get(mRequests.size() - 1).state; + } else { + mMergedState = mBaseState; + } + + if (mMergedState != originalMergedState) { + for (IDeviceStateManagerCallback callback : mCallbacks) { + try { + callback.onDeviceStateChanged(mMergedState); } catch (RemoteException e) { // Do nothing. Should never happen. } diff --git a/services/core/java/com/android/server/devicestate/DeviceState.java b/services/core/java/com/android/server/devicestate/DeviceState.java index 802472fcfba8d..e496d77deaf5a 100644 --- a/services/core/java/com/android/server/devicestate/DeviceState.java +++ b/services/core/java/com/android/server/devicestate/DeviceState.java @@ -16,8 +16,6 @@ package com.android.server.devicestate; -import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; - import android.annotation.IntRange; import android.annotation.NonNull; @@ -37,16 +35,16 @@ import java.util.Objects; */ public final class DeviceState { /** Unique identifier for the device state. */ - @IntRange(from = INVALID_DEVICE_STATE) + @IntRange(from = 0) private final int mIdentifier; /** String description of the device state. */ @NonNull private final String mName; - public DeviceState(@IntRange(from = INVALID_DEVICE_STATE) int identifier, + public DeviceState(@IntRange(from = 0) int identifier, @NonNull String name) { - if (identifier != INVALID_DEVICE_STATE && identifier < 0) { + if (identifier < 0) { throw new IllegalArgumentException("Identifier must be greater than or equal to zero."); } mIdentifier = identifier; @@ -54,7 +52,7 @@ public final class DeviceState { } /** Returns the unique identifier for the device state. */ - @IntRange(from = INVALID_DEVICE_STATE) + @IntRange(from = 0) public int getIdentifier() { return mIdentifier; } diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index 375ec3a0f95f8..984a17694e07a 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java @@ -17,13 +17,13 @@ package com.android.server.devicestate; import static android.Manifest.permission.CONTROL_DEVICE_STATE; -import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; +import static android.hardware.devicestate.DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; -import android.content.pm.PackageManager; +import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.IDeviceStateManager; import android.hardware.devicestate.IDeviceStateManagerCallback; import android.os.Binder; @@ -31,6 +31,8 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; @@ -62,8 +64,12 @@ import java.util.Optional; * 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. *

+ * The service also provides the {@link DeviceStateManager} API allowing clients to listen for + * changes in device state and submit requests to override the device state provided by the + * {@link DeviceStateProvider}. * * @see DeviceStatePolicy + * @see DeviceStateManager */ public final class DeviceStateManagerService extends SystemService { private static final String TAG = "DeviceStateManagerService"; @@ -79,11 +85,11 @@ public final class DeviceStateManagerService extends SystemService { @GuardedBy("mLock") private SparseArray mDeviceStates = new SparseArray<>(); - // The current committed device state. The default of INVALID_DEVICE_STATE will be replaced by + // The current committed device state. The default of UNSET will be replaced by // the current state after the initial callback from the DeviceStateProvider. @GuardedBy("mLock") @NonNull - private DeviceState mCommittedState = new DeviceState(INVALID_DEVICE_STATE, "INVALID"); + private DeviceState mCommittedState = new DeviceState(0, "UNSET"); // The device state that is currently awaiting callback from the policy to be committed. @GuardedBy("mLock") @NonNull @@ -91,19 +97,23 @@ public final class DeviceStateManagerService extends SystemService { // 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. - // Can be overwritten by an override state value if requested. - @GuardedBy("mLock") - @NonNull - private Optional mRequestedState = Optional.empty(); - // The most recently requested override state, or empty if no override is requested. - @GuardedBy("mLock") - @NonNull - private Optional mRequestedOverrideState = Optional.empty(); - // List of registered callbacks indexed by process id. + // The device state that is set by the device state provider. @GuardedBy("mLock") - private final SparseArray mCallbacks = new SparseArray<>(); + @NonNull + private Optional mBaseState = Optional.empty(); + + // List of processes registered to receive notifications about changes to device state and + // request status indexed by process id. + @GuardedBy("mLock") + private final SparseArray mProcessRecords = new SparseArray<>(); + // List of override requests with the highest precedence request at the end. + @GuardedBy("mLock") + private final ArrayList mRequestRecords = new ArrayList<>(); + // Set of override requests that are pending a call to notifyStatusIfNeeded() to be notified + // of a change in status. + @GuardedBy("mLock") + private final ArraySet mRequestsPendingStatusChange = new ArraySet<>(); public DeviceStateManagerService(@NonNull Context context) { this(context, new DeviceStatePolicyImpl(context)); @@ -148,55 +158,32 @@ public final class DeviceStateManagerService extends SystemService { } /** - * Returns the requested state. The service will configure the device to match the requested - * state when possible. + * Returns the base state. The service will configure the device to match the base state when + * there is no active request to override the base state. + * + * @see #getOverrideState() */ @NonNull - Optional getRequestedState() { + Optional getBaseState() { synchronized (mLock) { - return mRequestedState; + return mBaseState; } } /** - * Overrides the current device state with the provided state. - * - * @return {@code true} if the override state is valid and supported, {@code false} otherwise. - */ - boolean setOverrideState(int overrideState) { - if (getContext().checkCallingOrSelfPermission(CONTROL_DEVICE_STATE) - != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Must hold permission " + CONTROL_DEVICE_STATE); - } - - synchronized (mLock) { - if (overrideState != INVALID_DEVICE_STATE && !isSupportedStateLocked(overrideState)) { - return false; - } - - mRequestedOverrideState = getStateLocked(overrideState); - updatePendingStateLocked(); - } - - notifyPolicyIfNeeded(); - return true; - } - - /** - * Clears an override state set with {@link #setOverrideState(int)}. - */ - void clearOverrideState() { - setOverrideState(INVALID_DEVICE_STATE); - } - - /** - * Returns the current requested override state, or {@link Optional#empty()} if no override - * state is requested. + * Returns the current override state, or {@link Optional#empty()} if no override state is + * requested. If an override states is present, the returned state will take precedence over + * the base state returned from {@link #getBaseState()}. */ @NonNull Optional getOverrideState() { synchronized (mLock) { - return mRequestedOverrideState; + if (mRequestRecords.isEmpty()) { + return Optional.empty(); + } + + OverrideRequestRecord topRequest = mRequestRecords.get(mRequestRecords.size() - 1); + return Optional.of(topRequest.mRequestedState); } } @@ -211,6 +198,17 @@ public final class DeviceStateManagerService extends SystemService { } } + /** Returns the list of currently supported device state identifiers. */ + private int[] getSupportedStateIdentifiers() { + synchronized (mLock) { + int[] supportedStates = new int[mDeviceStates.size()]; + for (int i = 0; i < supportedStates.length; i++) { + supportedStates[i] = mDeviceStates.valueAt(i).getIdentifier(); + } + return supportedStates; + } + } + @VisibleForTesting IDeviceStateManager getBinderService() { return mBinderService; @@ -224,22 +222,26 @@ public final class DeviceStateManagerService extends SystemService { mDeviceStates.put(state.getIdentifier(), state); } - if (mRequestedState.isPresent() - && !isSupportedStateLocked(mRequestedState.get().getIdentifier())) { - // The current requested state is no longer valid. We'll clear it here, though + if (mBaseState.isPresent() + && !isSupportedStateLocked(mBaseState.get().getIdentifier())) { + // The current base state is no longer valid. We'll clear it here, though // we won't actually update the current state until a callback comes from the // provider with the most recent state. - mRequestedState = Optional.empty(); + mBaseState = Optional.empty(); } - if (mRequestedOverrideState.isPresent() - && !isSupportedStateLocked(mRequestedOverrideState.get().getIdentifier())) { - // The current override state is no longer valid. We'll clear it here and update - // the committed state if necessary. - mRequestedOverrideState = Optional.empty(); + + final int requestSize = mRequestRecords.size(); + for (int i = 0; i < requestSize; i++) { + OverrideRequestRecord request = mRequestRecords.get(i); + if (!isSupportedStateLocked(request.mRequestedState.getIdentifier())) { + request.setStatusLocked(OverrideRequestRecord.STATUS_CANCELED); + } } + updatePendingStateLocked(); } + notifyRequestsOfStatusChangeIfNeeded(); notifyPolicyIfNeeded(); } @@ -261,20 +263,37 @@ public final class DeviceStateManagerService extends SystemService { } /** - * 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. + * Requests to set the base 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 identifier) { + private void setBaseState(int identifier) { synchronized (mLock) { - final Optional requestedState = getStateLocked(identifier); - if (requestedState.isPresent()) { - mRequestedState = requestedState; + if (mBaseState.isPresent() && mBaseState.get().getIdentifier() == identifier) { + // Base state hasn't changed. Nothing to do. + return; } + + final Optional baseState = getStateLocked(identifier); + if (!baseState.isPresent()) { + throw new IllegalArgumentException("Base state is not supported"); + } + + mBaseState = baseState; + + final int requestSize = mRequestRecords.size(); + for (int i = 0; i < requestSize; i++) { + OverrideRequestRecord request = mRequestRecords.get(i); + if ((request.mFlags & FLAG_CANCEL_WHEN_BASE_CHANGES) > 0) { + request.setStatusLocked(OverrideRequestRecord.STATUS_CANCELED); + } + } + updatePendingStateLocked(); } + notifyRequestsOfStatusChangeIfNeeded(); notifyPolicyIfNeeded(); } @@ -290,10 +309,10 @@ public final class DeviceStateManagerService extends SystemService { } final DeviceState stateToConfigure; - if (mRequestedOverrideState.isPresent()) { - stateToConfigure = mRequestedOverrideState.get(); + if (!mRequestRecords.isEmpty()) { + stateToConfigure = mRequestRecords.get(mRequestRecords.size() - 1).mRequestedState; } else { - stateToConfigure = mRequestedState.orElse(null); + stateToConfigure = mBaseState.orElse(null); } if (stateToConfigure == null) { @@ -360,6 +379,13 @@ public final class DeviceStateManagerService extends SystemService { } mCommittedState = mPendingState.get(); newState = mCommittedState.getIdentifier(); + + if (!mRequestRecords.isEmpty()) { + final OverrideRequestRecord topRequest = + mRequestRecords.get(mRequestRecords.size() - 1); + topRequest.setStatusLocked(OverrideRequestRecord.STATUS_ACTIVE); + } + mPendingState = Optional.empty(); updatePendingStateLocked(); } @@ -367,6 +393,9 @@ public final class DeviceStateManagerService extends SystemService { // Notify callbacks of a change. notifyDeviceStateChanged(newState); + // Notify the top request that it's active. + notifyRequestsOfStatusChangeIfNeeded(); + // Try to configure the next state if needed. notifyPolicyIfNeeded(); } @@ -377,43 +406,69 @@ public final class DeviceStateManagerService extends SystemService { "Attempting to notify callbacks with service lock held."); } - // Grab the lock and copy the callbacks. - ArrayList callbacks; + // Grab the lock and copy the process records. + ArrayList registeredProcesses; synchronized (mLock) { - if (mCallbacks.size() == 0) { + if (mProcessRecords.size() == 0) { return; } - callbacks = new ArrayList<>(); - for (int i = 0; i < mCallbacks.size(); i++) { - callbacks.add(mCallbacks.valueAt(i)); + registeredProcesses = new ArrayList<>(); + for (int i = 0; i < mProcessRecords.size(); i++) { + registeredProcesses.add(mProcessRecords.valueAt(i)); } } // After releasing the lock, send the notifications out. - for (int i = 0; i < callbacks.size(); i++) { - callbacks.get(i).notifyDeviceStateAsync(deviceState); + for (int i = 0; i < registeredProcesses.size(); i++) { + registeredProcesses.get(i).notifyDeviceStateAsync(deviceState); } } - private void registerCallbackInternal(IDeviceStateManagerCallback callback, int callingPid) { + /** + * Notifies all dirty requests (requests that have a change in status, but have not yet been + * notified) that their status has changed. + */ + private void notifyRequestsOfStatusChangeIfNeeded() { + if (Thread.holdsLock(mLock)) { + throw new IllegalStateException( + "Attempting to notify requests with service lock held."); + } + + ArraySet dirtyRequests; + synchronized (mLock) { + if (mRequestsPendingStatusChange.isEmpty()) { + return; + } + + dirtyRequests = new ArraySet<>(mRequestsPendingStatusChange); + mRequestsPendingStatusChange.clear(); + } + + // After releasing the lock, send the notifications out. + for (int i = 0; i < dirtyRequests.size(); i++) { + dirtyRequests.valueAt(i).notifyStatusIfNeeded(); + } + } + + private void registerProcess(int pid, IDeviceStateManagerCallback callback) { int currentState; - CallbackRecord record; + ProcessRecord record; // Grab the lock to register the callback and get the current state. synchronized (mLock) { - if (mCallbacks.contains(callingPid)) { + if (mProcessRecords.contains(pid)) { throw new SecurityException("The calling process has already registered an" + " IDeviceStateManagerCallback."); } - record = new CallbackRecord(callback, callingPid); + record = new ProcessRecord(callback, pid); try { callback.asBinder().linkToDeath(record, 0); } catch (RemoteException ex) { throw new RuntimeException(ex); } - mCallbacks.put(callingPid, record); + mProcessRecords.put(pid, record); currentState = mCommittedState.getIdentifier(); } @@ -421,10 +476,86 @@ public final class DeviceStateManagerService extends SystemService { record.notifyDeviceStateAsync(currentState); } - private void unregisterCallbackInternal(CallbackRecord record) { + private void handleProcessDied(ProcessRecord processRecord) { synchronized (mLock) { - mCallbacks.remove(record.mPid); + // Cancel all requests from this process. + final int requestCount = processRecord.mRequestRecords.size(); + for (int i = 0; i < requestCount; i++) { + final OverrideRequestRecord request = processRecord.mRequestRecords.valueAt(i); + // Cancel the request but don't mark it as dirty since there's no need to send + // notifications if the process has died. + request.setStatusLocked(OverrideRequestRecord.STATUS_CANCELED, + false /* markDirty */); + } + + mProcessRecords.remove(processRecord.mPid); + + updatePendingStateLocked(); } + + notifyPolicyIfNeeded(); + } + + private void requestStateInternal(int state, int flags, int callingPid, + @NonNull IBinder token) { + synchronized (mLock) { + final ProcessRecord processRecord = mProcessRecords.get(callingPid); + if (processRecord == null) { + throw new IllegalStateException("Process " + callingPid + + " has no registered callback."); + } + + if (processRecord.mRequestRecords.get(token) != null) { + throw new IllegalStateException("Request has already been made for the supplied" + + " token: " + token); + } + + final Optional deviceState = getStateLocked(state); + if (!deviceState.isPresent()) { + throw new IllegalArgumentException("Requested state: " + state + + " is not supported."); + } + + OverrideRequestRecord topRecord = mRequestRecords.isEmpty() + ? null : mRequestRecords.get(mRequestRecords.size() - 1); + if (topRecord != null) { + topRecord.setStatusLocked(OverrideRequestRecord.STATUS_SUSPENDED); + } + + final OverrideRequestRecord request = + new OverrideRequestRecord(processRecord, token, deviceState.get(), flags); + mRequestRecords.add(request); + processRecord.mRequestRecords.put(request.mToken, request); + // We don't set the status of the new request to ACTIVE here as it will be set in + // commitPendingState(). + + updatePendingStateLocked(); + } + + notifyRequestsOfStatusChangeIfNeeded(); + notifyPolicyIfNeeded(); + } + + private void cancelRequestInternal(int callingPid, @NonNull IBinder token) { + synchronized (mLock) { + final ProcessRecord processRecord = mProcessRecords.get(callingPid); + if (processRecord == null) { + throw new IllegalStateException("Process " + callingPid + + " has no registered callback."); + } + + OverrideRequestRecord request = processRecord.mRequestRecords.get(token); + if (request == null) { + throw new IllegalStateException("No known request for the given token"); + } + + request.setStatusLocked(OverrideRequestRecord.STATUS_CANCELED); + + updatePendingStateLocked(); + } + + notifyRequestsOfStatusChangeIfNeeded(); + notifyPolicyIfNeeded(); } private void dumpInternal(PrintWriter pw) { @@ -433,15 +564,26 @@ public final class DeviceStateManagerService extends SystemService { synchronized (mLock) { pw.println(" mCommittedState=" + mCommittedState); pw.println(" mPendingState=" + mPendingState); - pw.println(" mRequestedState=" + mRequestedState); - pw.println(" mRequestedOverrideState=" + mRequestedOverrideState); + pw.println(" mBaseState=" + mBaseState); + pw.println(" mOverrideState=" + getOverrideState()); - final int callbackCount = mCallbacks.size(); + final int processCount = mProcessRecords.size(); pw.println(); - pw.println("Callbacks: size=" + callbackCount); - for (int i = 0; i < callbackCount; i++) { - CallbackRecord callback = mCallbacks.valueAt(i); - pw.println(" " + i + ": mPid=" + callback.mPid); + pw.println("Registered processes: size=" + processCount); + for (int i = 0; i < processCount; i++) { + ProcessRecord processRecord = mProcessRecords.valueAt(i); + pw.println(" " + i + ": mPid=" + processRecord.mPid); + } + + final int requestCount = mRequestRecords.size(); + pw.println(); + pw.println("Override requests: size=" + requestCount); + for (int i = 0; i < requestCount; i++) { + OverrideRequestRecord requestRecord = mRequestRecords.get(i); + pw.println(" " + i + ": mPid=" + requestRecord.mProcessRecord.mPid + + ", mRequestedState=" + requestRecord.mRequestedState + + ", mFlags=" + requestRecord.mFlags + + ", mStatus=" + requestRecord.statusToString(requestRecord.mStatus)); } } } @@ -452,12 +594,6 @@ public final class DeviceStateManagerService extends SystemService { if (newDeviceStates.length == 0) { throw new IllegalArgumentException("Supported device states must not be empty"); } - for (int i = 0; i < newDeviceStates.length; i++) { - if (newDeviceStates[i].getIdentifier() == INVALID_DEVICE_STATE) { - throw new IllegalArgumentException( - "Supported device states includes INVALID_DEVICE_STATE identifier"); - } - } updateSupportedStates(newDeviceStates); } @@ -467,22 +603,24 @@ public final class DeviceStateManagerService extends SystemService { throw new IllegalArgumentException("Invalid identifier: " + identifier); } - requestState(identifier); + setBaseState(identifier); } } - private final class CallbackRecord implements IBinder.DeathRecipient { + private final class ProcessRecord implements IBinder.DeathRecipient { private final IDeviceStateManagerCallback mCallback; private final int mPid; - CallbackRecord(IDeviceStateManagerCallback callback, int pid) { + private final ArrayMap mRequestRecords = new ArrayMap<>(); + + ProcessRecord(IDeviceStateManagerCallback callback, int pid) { mCallback = callback; mPid = pid; } @Override public void binderDied() { - unregisterCallbackInternal(this); + handleProcessDied(this); } public void notifyDeviceStateAsync(int devicestate) { @@ -493,6 +631,119 @@ public final class DeviceStateManagerService extends SystemService { ex); } } + + public void notifyRequestActiveAsync(OverrideRequestRecord request) { + try { + mCallback.onRequestActive(request.mToken); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.", + ex); + } + } + + public void notifyRequestSuspendedAsync(OverrideRequestRecord request) { + try { + mCallback.onRequestSuspended(request.mToken); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.", + ex); + } + } + + public void notifyRequestCanceledAsync(OverrideRequestRecord request) { + try { + mCallback.onRequestCanceled(request.mToken); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.", + ex); + } + } + } + + /** A record describing a request to override the state of the device. */ + private final class OverrideRequestRecord { + public static final int STATUS_UNKNOWN = 0; + public static final int STATUS_ACTIVE = 1; + public static final int STATUS_SUSPENDED = 2; + public static final int STATUS_CANCELED = 3; + + @Nullable + public String statusToString(int status) { + switch (status) { + case STATUS_ACTIVE: + return "ACTIVE"; + case STATUS_SUSPENDED: + return "SUSPENDED"; + case STATUS_CANCELED: + return "CANCELED"; + case STATUS_UNKNOWN: + return "UNKNOWN"; + default: + return null; + } + } + + private final ProcessRecord mProcessRecord; + @NonNull + private final IBinder mToken; + @NonNull + private final DeviceState mRequestedState; + private final int mFlags; + + private int mStatus = STATUS_UNKNOWN; + private int mLastNotifiedStatus = STATUS_UNKNOWN; + + OverrideRequestRecord(@NonNull ProcessRecord processRecord, @NonNull IBinder token, + @NonNull DeviceState requestedState, int flags) { + mProcessRecord = processRecord; + mToken = token; + mRequestedState = requestedState; + mFlags = flags; + } + + public void setStatusLocked(int status) { + setStatusLocked(status, true /* markDirty */); + } + + public void setStatusLocked(int status, boolean markDirty) { + if (mStatus != status) { + if (mStatus == STATUS_CANCELED) { + throw new IllegalStateException( + "Can not alter the status of a request after set to CANCELED."); + } + + mStatus = status; + + if (mStatus == STATUS_CANCELED) { + mRequestRecords.remove(this); + mProcessRecord.mRequestRecords.remove(mToken); + } + + if (markDirty) { + mRequestsPendingStatusChange.add(this); + } + } + } + + public void notifyStatusIfNeeded() { + int stateToReport; + synchronized (mLock) { + if (mLastNotifiedStatus == mStatus) { + return; + } + + stateToReport = mStatus; + mLastNotifiedStatus = mStatus; + } + + if (stateToReport == STATUS_ACTIVE) { + mProcessRecord.notifyRequestActiveAsync(this); + } else if (stateToReport == STATUS_SUSPENDED) { + mProcessRecord.notifyRequestSuspendedAsync(this); + } else if (stateToReport == STATUS_CANCELED) { + mProcessRecord.notifyRequestCanceledAsync(this); + } + } } /** Implementation of {@link IDeviceStateManager} published as a binder service. */ @@ -506,12 +757,58 @@ public final class DeviceStateManagerService extends SystemService { final int callingPid = Binder.getCallingPid(); final long token = Binder.clearCallingIdentity(); try { - registerCallbackInternal(callback, callingPid); + registerProcess(callingPid, callback); } finally { Binder.restoreCallingIdentity(token); } } + @Override // Binder call + public int[] getSupportedDeviceStates() { + final long token = Binder.clearCallingIdentity(); + try { + return getSupportedStateIdentifiers(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override // Binder call + public void requestState(IBinder token, int state, int flags) { + getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE, + "Permission required to request device state."); + + if (token == null) { + throw new IllegalArgumentException("Request token must not be null."); + } + + final int callingPid = Binder.getCallingPid(); + final long callingIdentity = Binder.clearCallingIdentity(); + try { + requestStateInternal(state, flags, callingPid, token); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + + @Override // Binder call + public void cancelRequest(IBinder token) { + getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE, + "Permission required to clear requested device state."); + + if (token == null) { + throw new IllegalArgumentException("Request token must not be null."); + } + + final int callingPid = Binder.getCallingPid(); + final long callingIdentity = Binder.clearCallingIdentity(); + try { + cancelRequestInternal(callingPid, token); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + @Override // Binder call public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver result) { diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java index 7914531f99100..6cc55a6c4774f 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java @@ -16,6 +16,13 @@ package com.android.server.devicestate; +import static android.Manifest.permission.CONTROL_DEVICE_STATE; + +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.devicestate.DeviceStateManager; +import android.hardware.devicestate.DeviceStateRequest; +import android.os.Binder; import android.os.ShellCommand; import java.io.PrintWriter; @@ -27,10 +34,15 @@ import java.util.Optional; * Use with {@code adb shell cmd device_state ...}. */ public class DeviceStateManagerShellCommand extends ShellCommand { - private final DeviceStateManagerService mInternal; + @Nullable + private static DeviceStateRequest sLastRequest; + + private final DeviceStateManagerService mService; + private final DeviceStateManager mClient; public DeviceStateManagerShellCommand(DeviceStateManagerService service) { - mInternal = service; + mService = service; + mClient = service.getContext().getSystemService(DeviceStateManager.class); } @Override @@ -51,15 +63,15 @@ public class DeviceStateManagerShellCommand extends ShellCommand { } private void printState(PrintWriter pw) { - DeviceState committedState = mInternal.getCommittedState(); - Optional requestedState = mInternal.getRequestedState(); - Optional requestedOverrideState = mInternal.getOverrideState(); + DeviceState committedState = mService.getCommittedState(); + Optional baseState = mService.getBaseState(); + Optional overrideState = mService.getOverrideState(); pw.println("Committed state: " + committedState); - if (requestedOverrideState.isPresent()) { + if (overrideState.isPresent()) { pw.println("----------------------"); - pw.println("Base state: " + requestedState.orElse(null)); - pw.println("Override state: " + requestedOverrideState.get()); + pw.println("Base state: " + baseState.orElse(null)); + pw.println("Override state: " + overrideState.get()); } } @@ -67,32 +79,51 @@ public class DeviceStateManagerShellCommand extends ShellCommand { final String nextArg = getNextArg(); if (nextArg == null) { printState(pw); - } else if ("reset".equals(nextArg)) { - mInternal.clearOverrideState(); - } else { - int requestedState; - try { - requestedState = Integer.parseInt(nextArg); - } catch (NumberFormatException e) { - getErrPrintWriter().println("Error: requested state should be an integer"); - return -1; - } - - boolean success = mInternal.setOverrideState(requestedState); - if (!success) { - getErrPrintWriter().println("Error: failed to set override state. Run:"); - getErrPrintWriter().println(""); - getErrPrintWriter().println(" print-states"); - getErrPrintWriter().println(""); - getErrPrintWriter().println("to get the list of currently supported device states"); - return -1; - } } + + final Context context = mService.getContext(); + context.enforceCallingOrSelfPermission( + CONTROL_DEVICE_STATE, + "Permission required to request device state."); + final long callingIdentity = Binder.clearCallingIdentity(); + try { + if ("reset".equals(nextArg)) { + if (sLastRequest != null) { + mClient.cancelRequest(sLastRequest); + sLastRequest = null; + } + } else { + int requestedState = Integer.parseInt(nextArg); + DeviceStateRequest request = DeviceStateRequest.newBuilder(requestedState).build(); + + mClient.requestState(request, null /* executor */, null /* callback */); + if (sLastRequest != null) { + mClient.cancelRequest(sLastRequest); + } + + sLastRequest = request; + } + } catch (NumberFormatException e) { + getErrPrintWriter().println("Error: requested state should be an integer"); + return -1; + } catch (IllegalArgumentException e) { + getErrPrintWriter().println("Error: " + e.getMessage()); + getErrPrintWriter().println("-------------------"); + getErrPrintWriter().println("Run:"); + getErrPrintWriter().println(""); + getErrPrintWriter().println(" print-states"); + getErrPrintWriter().println(""); + getErrPrintWriter().println("to get the list of currently supported device states"); + return -1; + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + return 0; } private int runPrintStates(PrintWriter pw) { - DeviceState[] states = mInternal.getSupportedStates(); + DeviceState[] states = mService.getSupportedStates(); pw.print("Supported states: [\n"); for (int i = 0; i < states.length; i++) { pw.print(" " + states[i] + ",\n"); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index e0baee70b819f..242d8d30aa5c1 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -513,8 +513,8 @@ public final class DisplayManagerService extends SystemService { DeviceStateManager deviceStateManager = mContext.getSystemService(DeviceStateManager.class); - deviceStateManager.registerDeviceStateListener(new DeviceStateListener(), - new HandlerExecutor(mHandler)); + deviceStateManager.addDeviceStateListener(new HandlerExecutor(mHandler), + new DeviceStateListener()); scheduleTraversalLocked(false); } diff --git a/services/core/java/com/android/server/policy/DisplayFoldController.java b/services/core/java/com/android/server/policy/DisplayFoldController.java index c10e828d8c3dc..82fc22c51afc9 100644 --- a/services/core/java/com/android/server/policy/DisplayFoldController.java +++ b/services/core/java/com/android/server/policy/DisplayFoldController.java @@ -74,8 +74,8 @@ class DisplayFoldController { mHandler = handler; DeviceStateManager deviceStateManager = context.getSystemService(DeviceStateManager.class); - deviceStateManager.registerDeviceStateListener(new DeviceStateListener(context), - new HandlerExecutor(handler)); + deviceStateManager.addDeviceStateListener(new HandlerExecutor(handler), + new DeviceStateListener(context)); } void finishedGoingToSleep() { diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java index 54da6436ad898..1a2266139405d 100644 --- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java @@ -19,10 +19,14 @@ package com.android.server.devicestate; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertThrows; +import android.hardware.devicestate.DeviceStateRequest; import android.hardware.devicestate.IDeviceStateManagerCallback; +import android.os.Binder; +import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; @@ -33,6 +37,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.HashMap; import java.util.Optional; import javax.annotation.Nullable; @@ -61,87 +66,69 @@ public final class DeviceStateManagerServiceTest { } @Test - public void requestStateChange() { + public void baseStateChanged() { assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); assertEquals(mService.getPendingState(), Optional.empty()); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE.getIdentifier()); - mProvider.notifyRequestState(OTHER_DEVICE_STATE.getIdentifier()); + mProvider.setState(OTHER_DEVICE_STATE.getIdentifier()); assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE); assertEquals(mService.getPendingState(), Optional.empty()); - assertEquals(mService.getRequestedState().get(), OTHER_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), OTHER_DEVICE_STATE); assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), OTHER_DEVICE_STATE.getIdentifier()); } @Test - public void requestStateChange_pendingState() { + public void baseStateChanged_withStatePendingPolicyCallback() { mPolicy.blockConfigure(); - mProvider.notifyRequestState(OTHER_DEVICE_STATE.getIdentifier()); + mProvider.setState(OTHER_DEVICE_STATE.getIdentifier()); assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); assertEquals(mService.getPendingState().get(), OTHER_DEVICE_STATE); - assertEquals(mService.getRequestedState().get(), OTHER_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), OTHER_DEVICE_STATE); assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), OTHER_DEVICE_STATE.getIdentifier()); - mProvider.notifyRequestState(DEFAULT_DEVICE_STATE.getIdentifier()); + mProvider.setState(DEFAULT_DEVICE_STATE.getIdentifier()); assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); assertEquals(mService.getPendingState().get(), OTHER_DEVICE_STATE); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), OTHER_DEVICE_STATE.getIdentifier()); mPolicy.resumeConfigure(); assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); assertEquals(mService.getPendingState(), Optional.empty()); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE.getIdentifier()); } @Test - public void requestStateChange_unsupportedState() { - mProvider.notifyRequestState(UNSUPPORTED_DEVICE_STATE.getIdentifier()); + public void baseStateChanged_unsupportedState() { + assertThrows(IllegalArgumentException.class, () -> { + mProvider.setState(UNSUPPORTED_DEVICE_STATE.getIdentifier()); + }); + assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); assertEquals(mService.getPendingState(), Optional.empty()); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE.getIdentifier()); } @Test - public void requestStateChange_invalidState() { + public void baseStateChanged_invalidState() { assertThrows(IllegalArgumentException.class, () -> { - mProvider.notifyRequestState(INVALID_DEVICE_STATE); + mProvider.setState(INVALID_DEVICE_STATE); }); - } - @Test - public void requestOverrideState() { - mService.setOverrideState(OTHER_DEVICE_STATE.getIdentifier()); - // Committed state changes as there is a requested override. - assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); - assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), - OTHER_DEVICE_STATE.getIdentifier()); - - // Committed state is set back to the requested state once the override is cleared. - mService.clearOverrideState(); assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); - assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), - DEFAULT_DEVICE_STATE.getIdentifier()); - } - - @Test - public void requestOverrideState_unsupportedState() { - mService.setOverrideState(UNSUPPORTED_DEVICE_STATE.getIdentifier()); - // Committed state remains the same as the override state is unsupported. - assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getPendingState(), Optional.empty()); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE.getIdentifier()); } @@ -150,7 +137,7 @@ public final class DeviceStateManagerServiceTest { public void supportedStatesChanged() { assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); assertEquals(mService.getPendingState(), Optional.empty()); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); mProvider.notifySupportedDeviceStates(new DeviceState[]{ DEFAULT_DEVICE_STATE }); @@ -158,46 +145,27 @@ public final class DeviceStateManagerServiceTest { // supported. assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); assertEquals(mService.getPendingState(), Optional.empty()); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); } @Test - public void supportedStatesChanged_unsupportedRequestedState() { + public void supportedStatesChanged_unsupportedBaseState() { assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); assertEquals(mService.getPendingState(), Optional.empty()); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); mProvider.notifySupportedDeviceStates(new DeviceState[]{ OTHER_DEVICE_STATE }); // The current requested state is cleared because it is no longer supported. assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); assertEquals(mService.getPendingState(), Optional.empty()); - assertEquals(mService.getRequestedState(), Optional.empty()); + assertEquals(mService.getBaseState(), Optional.empty()); - mProvider.notifyRequestState(OTHER_DEVICE_STATE.getIdentifier()); + mProvider.setState(OTHER_DEVICE_STATE.getIdentifier()); assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE); assertEquals(mService.getPendingState(), Optional.empty()); - assertEquals(mService.getRequestedState().get(), OTHER_DEVICE_STATE); - } - - @Test - public void supportedStatesChanged_unsupportedOverrideState() { - mService.setOverrideState(OTHER_DEVICE_STATE.getIdentifier()); - // Committed state changes as there is a requested override. - assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); - assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), - OTHER_DEVICE_STATE.getIdentifier()); - - mProvider.notifySupportedDeviceStates(new DeviceState[]{ DEFAULT_DEVICE_STATE }); - - // Committed state is set back to the requested state as the override state is no longer - // supported. - assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); - assertEquals(mService.getRequestedState().get(), DEFAULT_DEVICE_STATE); - assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), - DEFAULT_DEVICE_STATE.getIdentifier()); + assertEquals(mService.getBaseState().get(), OTHER_DEVICE_STATE); } @Test @@ -205,17 +173,17 @@ public final class DeviceStateManagerServiceTest { TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); mService.getBinderService().registerCallback(callback); - mProvider.notifyRequestState(OTHER_DEVICE_STATE.getIdentifier()); + mProvider.setState(OTHER_DEVICE_STATE.getIdentifier()); assertNotNull(callback.getLastNotifiedValue()); assertEquals(callback.getLastNotifiedValue().intValue(), OTHER_DEVICE_STATE.getIdentifier()); - mProvider.notifyRequestState(DEFAULT_DEVICE_STATE.getIdentifier()); + mProvider.setState(DEFAULT_DEVICE_STATE.getIdentifier()); assertEquals(callback.getLastNotifiedValue().intValue(), DEFAULT_DEVICE_STATE.getIdentifier()); mPolicy.blockConfigure(); - mProvider.notifyRequestState(OTHER_DEVICE_STATE.getIdentifier()); + mProvider.setState(OTHER_DEVICE_STATE.getIdentifier()); // The callback should not have been notified of the state change as the policy is still // pending callback. assertEquals(callback.getLastNotifiedValue().intValue(), @@ -237,6 +205,148 @@ public final class DeviceStateManagerServiceTest { DEFAULT_DEVICE_STATE.getIdentifier()); } + @Test + public void getSupportedDeviceStates() throws RemoteException { + final int[] expectedStates = new int[] { 0, 1 }; + assertEquals(mService.getBinderService().getSupportedDeviceStates(), expectedStates); + } + + @Test + public void requestState() throws RemoteException { + TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); + mService.getBinderService().registerCallback(callback); + + final IBinder token = new Binder(); + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_UNKNOWN); + + mService.getBinderService().requestState(token, OTHER_DEVICE_STATE.getIdentifier(), + 0 /* flags */); + + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_ACTIVE); + // Committed state changes as there is a requested override. + assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getOverrideState().get(), OTHER_DEVICE_STATE); + assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), + OTHER_DEVICE_STATE.getIdentifier()); + + + mService.getBinderService().cancelRequest(token); + + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_CANCELED); + // Committed state is set back to the requested state once the override is cleared. + assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); + assertFalse(mService.getOverrideState().isPresent()); + assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), + DEFAULT_DEVICE_STATE.getIdentifier()); + } + + @Test + public void requestState_flagCancelWhenBaseChanges() throws RemoteException { + TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); + mService.getBinderService().registerCallback(callback); + + final IBinder token = new Binder(); + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_UNKNOWN); + + mService.getBinderService().requestState(token, OTHER_DEVICE_STATE.getIdentifier(), + DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES); + + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_ACTIVE); + + // Committed state changes as there is a requested override. + assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getOverrideState().get(), OTHER_DEVICE_STATE); + assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), + OTHER_DEVICE_STATE.getIdentifier()); + + mProvider.setState(OTHER_DEVICE_STATE.getIdentifier()); + + // Request is canceled because the base state changed. + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_CANCELED); + // Committed state is set back to the requested state once the override is cleared. + assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), OTHER_DEVICE_STATE); + assertFalse(mService.getOverrideState().isPresent()); + assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), + OTHER_DEVICE_STATE.getIdentifier()); + } + + @Test + public void requestState_becomesUnsupported() throws RemoteException { + TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); + mService.getBinderService().registerCallback(callback); + + final IBinder token = new Binder(); + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_UNKNOWN); + + mService.getBinderService().requestState(token, OTHER_DEVICE_STATE.getIdentifier(), + 0 /* flags */); + + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_ACTIVE); + // Committed state changes as there is a requested override. + assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getOverrideState().get(), OTHER_DEVICE_STATE); + assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), + OTHER_DEVICE_STATE.getIdentifier()); + + mProvider.notifySupportedDeviceStates(new DeviceState[]{ DEFAULT_DEVICE_STATE }); + + // Request is canceled because the state is no longer supported. + assertEquals(callback.getLastNotifiedStatus(token), + TestDeviceStateManagerCallback.STATUS_CANCELED); + // Committed state is set back to the requested state as the override state is no longer + // supported. + assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE); + assertEquals(mService.getBaseState().get(), DEFAULT_DEVICE_STATE); + assertFalse(mService.getOverrideState().isPresent()); + assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), + DEFAULT_DEVICE_STATE.getIdentifier()); + } + + @Test + public void requestState_unsupportedState() throws RemoteException { + TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); + mService.getBinderService().registerCallback(callback); + + assertThrows(IllegalArgumentException.class, () -> { + final IBinder token = new Binder(); + mService.getBinderService().requestState(token, + UNSUPPORTED_DEVICE_STATE.getIdentifier(), 0 /* flags */); + }); + } + + @Test + public void requestState_invalidState() throws RemoteException { + TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); + mService.getBinderService().registerCallback(callback); + + assertThrows(IllegalArgumentException.class, () -> { + final IBinder token = new Binder(); + mService.getBinderService().requestState(token, INVALID_DEVICE_STATE, 0 /* flags */); + }); + } + + @Test + public void requestState_beforeRegisteringCallback() { + assertThrows(IllegalStateException.class, () -> { + final IBinder token = new Binder(); + mService.getBinderService().requestState(token, DEFAULT_DEVICE_STATE.getIdentifier(), + 0 /* flags */); + }); + } + private static final class TestDeviceStatePolicy implements DeviceStatePolicy { private final DeviceStateProvider mProvider; private int mLastDeviceStateRequestedToConfigure = INVALID_DEVICE_STATE; @@ -306,23 +416,48 @@ public final class DeviceStateManagerServiceTest { mListener.onSupportedDeviceStatesChanged(supportedDeviceStates); } - public void notifyRequestState(int identifier) { + public void setState(int identifier) { mListener.onStateChanged(identifier); } } private static final class TestDeviceStateManagerCallback extends IDeviceStateManagerCallback.Stub { - Integer mLastNotifiedValue; + public static final int STATUS_UNKNOWN = 0; + public static final int STATUS_ACTIVE = 1; + public static final int STATUS_SUSPENDED = 2; + public static final int STATUS_CANCELED = 3; + + private Integer mLastNotifiedValue; + private final HashMap mLastNotifiedStatus = new HashMap<>(); @Override public void onDeviceStateChanged(int deviceState) { mLastNotifiedValue = deviceState; } + @Override + public void onRequestActive(IBinder token) { + mLastNotifiedStatus.put(token, STATUS_ACTIVE); + } + + @Override + public void onRequestSuspended(IBinder token) { + mLastNotifiedStatus.put(token, STATUS_SUSPENDED); + } + + @Override + public void onRequestCanceled(IBinder token) { + mLastNotifiedStatus.put(token, STATUS_CANCELED); + } + @Nullable Integer getLastNotifiedValue() { return mLastNotifiedValue; } + + int getLastNotifiedStatus(IBinder requestToken) { + return mLastNotifiedStatus.getOrDefault(requestToken, STATUS_UNKNOWN); + } } }