From a5ab448ff0bfe59b92abe8f5f0864bd0e3429ba4 Mon Sep 17 00:00:00 2001
From: Darryl L Johnson
Date: Fri, 2 Jul 2021 11:42:09 -0700
Subject: [PATCH] Refactor request lifecycle logic into
OverrideRequestController.
This refactors the request lifecycle login into a controller,
OverrideRequestController, which simplies the login within
DeviceStateManagerService itself.
Bug: 192671286
Test: atest DeviceStateManagerServiceTest
Test: atest OverrideRequestControllerTest
Test: atest DeviceStateManagerTests
Change-Id: Ifaf04665bf8c84d62fa08b3b8883559c6726d6af
---
.../DeviceStateManagerService.java | 506 ++++++++----------
.../server/devicestate/OverrideRequest.java | 58 ++
.../OverrideRequestController.java | 290 ++++++++++
.../DeviceStateManagerServiceTest.java | 49 +-
.../OverrideRequestControllerTest.java | 204 +++++++
5 files changed, 812 insertions(+), 295 deletions(-)
create mode 100644 services/core/java/com/android/server/devicestate/OverrideRequest.java
create mode 100644 services/core/java/com/android/server/devicestate/OverrideRequestController.java
create mode 100644 services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index a8b0994402e80..42fd9642c2db7 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -19,8 +19,13 @@ package com.android.server.devicestate;
import static android.Manifest.permission.CONTROL_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
-import static android.hardware.devicestate.DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES;
+import static android.os.Process.THREAD_PRIORITY_DISPLAY;
+import static com.android.server.devicestate.OverrideRequestController.STATUS_ACTIVE;
+import static com.android.server.devicestate.OverrideRequestController.STATUS_CANCELED;
+import static com.android.server.devicestate.OverrideRequestController.STATUS_SUSPENDED;
+
+import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -30,12 +35,12 @@ import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.IDeviceStateManager;
import android.hardware.devicestate.IDeviceStateManagerCallback;
import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
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;
@@ -43,14 +48,18 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.ServiceThread;
import com.android.server.SystemService;
import com.android.server.policy.DeviceStatePolicyImpl;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Optional;
+import java.util.WeakHashMap;
/**
* A system service that manages the state of a device with user-configurable hardware like a
@@ -81,10 +90,17 @@ public final class DeviceStateManagerService extends SystemService {
private static final boolean DEBUG = false;
private final Object mLock = new Object();
+ // Internal system service thread used to dispatch calls to the policy and to registered
+ // callbacks though its handler (mHandler). Provides a guarantee of callback order when
+ // leveraging mHandler and also enables posting messages with the service lock held.
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
@NonNull
private final DeviceStatePolicy mDeviceStatePolicy;
@NonNull
private final BinderService mBinderService;
+ @NonNull
+ private final OverrideRequestController mOverrideRequestController;
// All supported device states keyed by identifier.
@GuardedBy("mLock")
@@ -109,17 +125,16 @@ public final class DeviceStateManagerService extends SystemService {
@NonNull
private Optional mBaseState = Optional.empty();
+ // The current active override request. When set the device state specified here will take
+ // precedence over mBaseState.
+ @GuardedBy("mLock")
+ @NonNull
+ private Optional mActiveOverride = 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));
@@ -128,6 +143,13 @@ public final class DeviceStateManagerService extends SystemService {
@VisibleForTesting
DeviceStateManagerService(@NonNull Context context, @NonNull DeviceStatePolicy policy) {
super(context);
+ // Service thread assigned THREAD_PRIORITY_DISPLAY because this service indirectly drives
+ // display (on/off) and window (position) events through its callbacks.
+ mHandlerThread = new ServiceThread(TAG, THREAD_PRIORITY_DISPLAY, false /* allowIo */);
+ mHandlerThread.start();
+ mHandler = mHandlerThread.getThreadHandler();
+ mOverrideRequestController = new OverrideRequestController(
+ this::onOverrideRequestStatusChangedLocked);
mDeviceStatePolicy = policy;
mDeviceStatePolicy.getDeviceStateProvider().setListener(new DeviceStateProviderListener());
mBinderService = new BinderService();
@@ -138,6 +160,11 @@ public final class DeviceStateManagerService extends SystemService {
publishBinderService(Context.DEVICE_STATE_SERVICE, mBinderService);
}
+ @VisibleForTesting
+ Handler getHandler() {
+ return mHandler;
+ }
+
/**
* Returns the current state the system is in. Note that the system may be in the process of
* configuring a different state.
@@ -191,12 +218,10 @@ public final class DeviceStateManagerService extends SystemService {
@NonNull
Optional getOverrideState() {
synchronized (mLock) {
- if (mRequestRecords.isEmpty()) {
- return Optional.empty();
+ if (mActiveOverride.isPresent()) {
+ return getStateLocked(mActiveOverride.get().getRequestedState());
}
-
- OverrideRequestRecord topRequest = mRequestRecords.get(mRequestRecords.size() - 1);
- return Optional.of(topRequest.mRequestedState);
+ return Optional.empty();
}
}
@@ -247,8 +272,6 @@ public final class DeviceStateManagerService extends SystemService {
}
private void updateSupportedStates(DeviceState[] supportedDeviceStates) {
- boolean updatedPendingState;
- boolean hasBaseState;
synchronized (mLock) {
final int[] oldStateIdentifiers = getSupportedStateIdentifiersLocked();
@@ -263,27 +286,18 @@ public final class DeviceStateManagerService extends SystemService {
return;
}
- 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);
- }
+ mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers);
+ updatePendingStateLocked();
+
+ if (!mPendingState.isPresent()) {
+ // If the change in the supported states didn't result in a change of the pending
+ // state commitPendingState() will never be called and the callbacks will never be
+ // notified of the change.
+ notifyDeviceStateInfoChangedAsync();
}
- updatedPendingState = updatePendingStateLocked();
- hasBaseState = mBaseState.isPresent();
+ mHandler.post(this::notifyPolicyIfNeeded);
}
-
- if (hasBaseState && !updatedPendingState) {
- // If the change in the supported states didn't result in a change of the pending state
- // commitPendingState() will never be called and the callbacks will never be notified
- // of the change.
- notifyDeviceStateInfoChanged();
- }
-
- notifyRequestsOfStatusChangeIfNeeded();
- notifyPolicyIfNeeded();
}
/**
@@ -311,7 +325,6 @@ public final class DeviceStateManagerService extends SystemService {
* @see #isSupportedStateLocked(int)
*/
private void setBaseState(int identifier) {
- boolean updatedPendingState;
synchronized (mLock) {
final Optional baseStateOptional = getStateLocked(identifier);
if (!baseStateOptional.isPresent()) {
@@ -325,26 +338,18 @@ public final class DeviceStateManagerService extends SystemService {
}
mBaseState = Optional.of(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);
- }
+ mOverrideRequestController.handleBaseStateChanged();
+ updatePendingStateLocked();
+
+ if (!mPendingState.isPresent()) {
+ // If the change in base state didn't result in a change of the pending state
+ // commitPendingState() will never be called and the callbacks will never be
+ // notified of the change.
+ notifyDeviceStateInfoChangedAsync();
}
- updatedPendingState = updatePendingStateLocked();
+ mHandler.post(this::notifyPolicyIfNeeded);
}
-
- if (!updatedPendingState) {
- // If the change in base state didn't result in a change of the pending state
- // commitPendingState() will never be called and the callbacks will never be notified
- // of the change.
- notifyDeviceStateInfoChanged();
- }
-
- notifyRequestsOfStatusChangeIfNeeded();
- notifyPolicyIfNeeded();
}
/**
@@ -362,8 +367,8 @@ public final class DeviceStateManagerService extends SystemService {
}
final DeviceState stateToConfigure;
- if (!mRequestRecords.isEmpty()) {
- stateToConfigure = mRequestRecords.get(mRequestRecords.size() - 1).mRequestedState;
+ if (mActiveOverride.isPresent()) {
+ stateToConfigure = getStateLocked(mActiveOverride.get().getRequestedState()).get();
} else if (mBaseState.isPresent()
&& isSupportedStateLocked(mBaseState.get().getIdentifier())) {
// Base state could have recently become unsupported after a change in supported states.
@@ -429,108 +434,106 @@ public final class DeviceStateManagerService extends SystemService {
*
*/
private void commitPendingState() {
- // Update the current state.
synchronized (mLock) {
final DeviceState newState = mPendingState.get();
if (DEBUG) {
Slog.d(TAG, "Committing state: " + newState);
}
- if (!mRequestRecords.isEmpty()) {
- final OverrideRequestRecord topRequest =
- mRequestRecords.get(mRequestRecords.size() - 1);
- if (topRequest.mRequestedState.getIdentifier() == newState.getIdentifier()) {
- // The top request could have come in while the service was awaiting callback
- // from the policy. In that case we only set it to active if it matches the
- // current committed state, otherwise it will be set to active when its
- // requested state is committed.
- topRequest.setStatusLocked(OverrideRequestRecord.STATUS_ACTIVE);
- }
- }
-
FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_STATE_CHANGED,
newState.getIdentifier(), !mCommittedState.isPresent());
mCommittedState = Optional.of(newState);
mPendingState = Optional.empty();
updatePendingStateLocked();
+
+ // Notify callbacks of a change.
+ notifyDeviceStateInfoChangedAsync();
+
+ // The top request could have come in while the service was awaiting callback
+ // from the policy. In that case we only set it to active if it matches the
+ // current committed state, otherwise it will be set to active when its
+ // requested state is committed.
+ OverrideRequest activeRequest = mActiveOverride.orElse(null);
+ if (activeRequest != null
+ && activeRequest.getRequestedState() == newState.getIdentifier()) {
+ ProcessRecord processRecord = mProcessRecords.get(activeRequest.getPid());
+ if (processRecord != null) {
+ processRecord.notifyRequestActiveAsync(activeRequest.getToken());
+ }
+ }
+
+ // Try to configure the next state if needed.
+ mHandler.post(this::notifyPolicyIfNeeded);
}
-
- // Notify callbacks of a change.
- notifyDeviceStateInfoChanged();
-
- // Notify the top request that it's active.
- notifyRequestsOfStatusChangeIfNeeded();
-
- // Try to configure the next state if needed.
- notifyPolicyIfNeeded();
}
- private void notifyDeviceStateInfoChanged() {
- if (Thread.holdsLock(mLock)) {
- throw new IllegalStateException(
- "Attempting to notify callbacks with service lock held.");
- }
-
- // Grab the lock and copy the process records and the current info.
- ArrayList registeredProcesses;
- DeviceStateInfo info;
+ private void notifyDeviceStateInfoChangedAsync() {
synchronized (mLock) {
if (mProcessRecords.size() == 0) {
return;
}
- registeredProcesses = new ArrayList<>();
+ ArrayList registeredProcesses = new ArrayList<>();
for (int i = 0; i < mProcessRecords.size(); i++) {
registeredProcesses.add(mProcessRecords.valueAt(i));
}
- info = getDeviceStateInfoLocked();
- }
+ DeviceStateInfo info = getDeviceStateInfoLocked();
- // After releasing the lock, send the notifications out.
- for (int i = 0; i < registeredProcesses.size(); i++) {
- registeredProcesses.get(i).notifyDeviceStateInfoAsync(info);
+ for (int i = 0; i < registeredProcesses.size(); i++) {
+ registeredProcesses.get(i).notifyDeviceStateInfoAsync(info);
+ }
}
}
- /**
- * 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;
+ private void onOverrideRequestStatusChangedLocked(@NonNull OverrideRequest request,
+ @OverrideRequestController.RequestStatus int status) {
+ if (status == STATUS_ACTIVE) {
+ mActiveOverride = Optional.of(request);
+ } else if (status == STATUS_SUSPENDED || status == STATUS_CANCELED) {
+ if (mActiveOverride.isPresent() && mActiveOverride.get() == request) {
+ mActiveOverride = Optional.empty();
}
-
- dirtyRequests = new ArraySet<>(mRequestsPendingStatusChange);
- mRequestsPendingStatusChange.clear();
+ } else {
+ throw new IllegalArgumentException("Unknown request status: " + status);
}
- // After releasing the lock, send the notifications out.
- for (int i = 0; i < dirtyRequests.size(); i++) {
- dirtyRequests.valueAt(i).notifyStatusIfNeeded();
+ boolean updatedPendingState = updatePendingStateLocked();
+
+ ProcessRecord processRecord = mProcessRecords.get(request.getPid());
+ if (processRecord == null) {
+ // If the process is no longer registered with the service, for example if it has died,
+ // there is no need to notify it of a change in request status.
+ mHandler.post(this::notifyPolicyIfNeeded);
+ return;
}
+
+ if (status == STATUS_ACTIVE) {
+ if (!updatedPendingState && !mPendingState.isPresent()) {
+ // If the pending state was not updated and there is not currently a pending state
+ // then this newly active request will never be notified of a change in state.
+ // Schedule the notification now.
+ processRecord.notifyRequestActiveAsync(request.getToken());
+ }
+ } else if (status == STATUS_SUSPENDED) {
+ processRecord.notifyRequestSuspendedAsync(request.getToken());
+ } else {
+ processRecord.notifyRequestCanceledAsync(request.getToken());
+ }
+
+ mHandler.post(this::notifyPolicyIfNeeded);
}
private void registerProcess(int pid, IDeviceStateManagerCallback callback) {
- DeviceStateInfo currentInfo;
- ProcessRecord record;
- // Grab the lock to register the callback and get the current state.
synchronized (mLock) {
if (mProcessRecords.contains(pid)) {
throw new SecurityException("The calling process has already registered an"
+ " IDeviceStateManagerCallback.");
}
- record = new ProcessRecord(callback, pid);
+ ProcessRecord record = new ProcessRecord(callback, pid, this::handleProcessDied,
+ mHandlerThread.getThreadHandler());
try {
callback.asBinder().linkToDeath(record, 0);
} catch (RemoteException ex) {
@@ -538,34 +541,21 @@ public final class DeviceStateManagerService extends SystemService {
}
mProcessRecords.put(pid, record);
- currentInfo = mCommittedState.isPresent() ? getDeviceStateInfoLocked() : null;
- }
-
- if (currentInfo != null) {
- // If there is not a committed state we'll wait to notify the process of the initial
- // value.
- record.notifyDeviceStateInfoAsync(currentInfo);
+ DeviceStateInfo currentInfo = mCommittedState.isPresent()
+ ? getDeviceStateInfoLocked() : null;
+ if (currentInfo != null) {
+ // If there is not a committed state we'll wait to notify the process of the initial
+ // value.
+ record.notifyDeviceStateInfoAsync(currentInfo);
+ }
}
}
private void handleProcessDied(ProcessRecord processRecord) {
synchronized (mLock) {
- // 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();
+ mOverrideRequestController.handleProcessDied(processRecord.mPid);
}
-
- notifyPolicyIfNeeded();
}
private void requestStateInternal(int state, int flags, int callingPid,
@@ -577,7 +567,7 @@ public final class DeviceStateManagerService extends SystemService {
+ " has no registered callback.");
}
- if (processRecord.mRequestRecords.get(token) != null) {
+ if (mOverrideRequestController.hasRequest(token)) {
throw new IllegalStateException("Request has already been made for the supplied"
+ " token: " + token);
}
@@ -588,27 +578,9 @@ public final class DeviceStateManagerService extends SystemService {
+ " 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);
-
- final boolean updatedPendingState = updatePendingStateLocked();
- if (!updatedPendingState && !mPendingState.isPresent()) {
- // We don't set the status of the new request to ACTIVE if the request updated the
- // pending state as it will be set in commitPendingState().
- request.setStatusLocked(OverrideRequestRecord.STATUS_ACTIVE, true /* markDirty */);
- }
+ OverrideRequest request = new OverrideRequest(token, callingPid, state, flags);
+ mOverrideRequestController.addRequest(request);
}
-
- notifyRequestsOfStatusChangeIfNeeded();
- notifyPolicyIfNeeded();
}
private void cancelRequestInternal(int callingPid, @NonNull IBinder token) {
@@ -619,18 +591,8 @@ public final class DeviceStateManagerService extends SystemService {
+ " 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();
+ mOverrideRequestController.cancelRequest(token);
}
-
- notifyRequestsOfStatusChangeIfNeeded();
- notifyPolicyIfNeeded();
}
private void dumpInternal(PrintWriter pw) {
@@ -650,16 +612,7 @@ public final class DeviceStateManagerService extends SystemService {
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));
- }
+ mOverrideRequestController.dumpInternal(pw);
}
}
@@ -683,142 +636,107 @@ public final class DeviceStateManagerService extends SystemService {
}
}
- private final class ProcessRecord implements IBinder.DeathRecipient {
+ private static final class ProcessRecord implements IBinder.DeathRecipient {
+ public interface DeathListener {
+ void onProcessDied(ProcessRecord record);
+ }
+
+ private static final int STATUS_ACTIVE = 0;
+
+ private static final int STATUS_SUSPENDED = 1;
+
+ private static final int STATUS_CANCELED = 2;
+
+ @IntDef(prefix = {"STATUS_"}, value = {
+ STATUS_ACTIVE,
+ STATUS_SUSPENDED,
+ STATUS_CANCELED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface RequestStatus {}
+
private final IDeviceStateManagerCallback mCallback;
private final int mPid;
+ private final DeathListener mDeathListener;
+ private final Handler mHandler;
- private final ArrayMap mRequestRecords = new ArrayMap<>();
+ private final WeakHashMap mLastNotifiedStatus = new WeakHashMap<>();
- ProcessRecord(IDeviceStateManagerCallback callback, int pid) {
+ ProcessRecord(IDeviceStateManagerCallback callback, int pid, DeathListener deathListener,
+ Handler handler) {
mCallback = callback;
mPid = pid;
+ mDeathListener = deathListener;
+ mHandler = handler;
}
@Override
public void binderDied() {
- handleProcessDied(this);
+ mDeathListener.onProcessDied(this);
}
public void notifyDeviceStateInfoAsync(@NonNull DeviceStateInfo info) {
- try {
- mCallback.onDeviceStateInfoChanged(info);
- } catch (RemoteException ex) {
- Slog.w(TAG, "Failed to notify process " + mPid + " that device state changed.",
- 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.");
+ mHandler.post(() -> {
+ try {
+ mCallback.onDeviceStateInfoChanged(info);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify process " + mPid + " that device state changed.",
+ ex);
}
-
- 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;
+ public void notifyRequestActiveAsync(IBinder token) {
+ @RequestStatus Integer lastStatus = mLastNotifiedStatus.get(token);
+ if (lastStatus != null
+ && (lastStatus == STATUS_ACTIVE || lastStatus == STATUS_CANCELED)) {
+ return;
+ }
+
+ mLastNotifiedStatus.put(token, STATUS_ACTIVE);
+ mHandler.post(() -> {
+ try {
+ mCallback.onRequestActive(token);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.",
+ ex);
}
+ });
+ }
- stateToReport = mStatus;
- mLastNotifiedStatus = mStatus;
+ public void notifyRequestSuspendedAsync(IBinder token) {
+ @RequestStatus Integer lastStatus = mLastNotifiedStatus.get(token);
+ if (lastStatus != null
+ && (lastStatus == STATUS_SUSPENDED || lastStatus == STATUS_CANCELED)) {
+ return;
}
- if (stateToReport == STATUS_ACTIVE) {
- mProcessRecord.notifyRequestActiveAsync(this);
- } else if (stateToReport == STATUS_SUSPENDED) {
- mProcessRecord.notifyRequestSuspendedAsync(this);
- } else if (stateToReport == STATUS_CANCELED) {
- mProcessRecord.notifyRequestCanceledAsync(this);
+ mLastNotifiedStatus.put(token, STATUS_SUSPENDED);
+ mHandler.post(() -> {
+ try {
+ mCallback.onRequestSuspended(token);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.",
+ ex);
+ }
+ });
+ }
+
+ public void notifyRequestCanceledAsync(IBinder token) {
+ @RequestStatus Integer lastStatus = mLastNotifiedStatus.get(token);
+ if (lastStatus != null && lastStatus == STATUS_CANCELED) {
+ return;
}
+
+ mLastNotifiedStatus.put(token, STATUS_CANCELED);
+ mHandler.post(() -> {
+ try {
+ mCallback.onRequestCanceled(token);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify process " + mPid + " that request state changed.",
+ ex);
+ }
+ });
}
}
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequest.java b/services/core/java/com/android/server/devicestate/OverrideRequest.java
new file mode 100644
index 0000000000000..35a4c844c710a
--- /dev/null
+++ b/services/core/java/com/android/server/devicestate/OverrideRequest.java
@@ -0,0 +1,58 @@
+/*
+ * 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 com.android.server.devicestate;
+
+import android.hardware.devicestate.DeviceStateRequest;
+import android.os.IBinder;
+
+/**
+ * A request to override the state managed by {@link DeviceStateManagerService}.
+ *
+ * @see OverrideRequestController
+ */
+final class OverrideRequest {
+ private final IBinder mToken;
+ private final int mPid;
+ private final int mRequestedState;
+ @DeviceStateRequest.RequestFlags
+ private final int mFlags;
+
+ OverrideRequest(IBinder token, int pid, int requestedState,
+ @DeviceStateRequest.RequestFlags int flags) {
+ mToken = token;
+ mPid = pid;
+ mRequestedState = requestedState;
+ mFlags = flags;
+ }
+
+ IBinder getToken() {
+ return mToken;
+ }
+
+ int getPid() {
+ return mPid;
+ }
+
+ int getRequestedState() {
+ return mRequestedState;
+ }
+
+ @DeviceStateRequest.RequestFlags
+ int getFlags() {
+ return mFlags;
+ }
+}
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequestController.java b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
new file mode 100644
index 0000000000000..042bea5fcfa37
--- /dev/null
+++ b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
@@ -0,0 +1,290 @@
+/*
+ * 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 com.android.server.devicestate;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.devicestate.DeviceStateRequest;
+import android.os.IBinder;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages the lifecycle of override requests.
+ *
+ * New requests are added with {@link #addRequest(OverrideRequest)} and are kept active until
+ * either:
+ *
+ * - A new request is added with {@link #addRequest(OverrideRequest)}, in which case the
+ * request will become suspended.
+ * - The request is cancelled with {@link #cancelRequest(IBinder)} or as a side effect
+ * of other methods calls, such as {@link #handleProcessDied(int)}.
+ *
+ */
+final class OverrideRequestController {
+ static final int STATUS_UNKNOWN = 0;
+ /**
+ * The request is the top-most request.
+ */
+ static final int STATUS_ACTIVE = 1;
+ /**
+ * The request is still present but is being superseded by another request.
+ */
+ static final int STATUS_SUSPENDED = 2;
+ /**
+ * The request is not longer valid.
+ */
+ static final int STATUS_CANCELED = 3;
+
+ @IntDef(prefix = {"STATUS_"}, value = {
+ STATUS_UNKNOWN,
+ STATUS_ACTIVE,
+ STATUS_SUSPENDED,
+ STATUS_CANCELED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RequestStatus {}
+
+ static String statusToString(@RequestStatus int status) {
+ switch (status) {
+ case STATUS_ACTIVE:
+ return "ACTIVE";
+ case STATUS_SUSPENDED:
+ return "SUSPENDED";
+ case STATUS_CANCELED:
+ return "CANCELED";
+ case STATUS_UNKNOWN:
+ return "UNKNOWN";
+ }
+ throw new IllegalArgumentException("Unknown status: " + status);
+ }
+
+ private final StatusChangeListener mListener;
+ private final List mTmpRequestsToCancel = new ArrayList<>();
+
+ // List of override requests with the most recent override request at the end.
+ private final ArrayList mRequests = new ArrayList<>();
+
+ OverrideRequestController(@NonNull StatusChangeListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Adds a request to the top of the stack and notifies the listener of all changes to request
+ * status as a result of this operation.
+ */
+ void addRequest(@NonNull OverrideRequest request) {
+ mRequests.add(request);
+ mListener.onStatusChanged(request, STATUS_ACTIVE);
+
+ if (mRequests.size() > 1) {
+ OverrideRequest prevRequest = mRequests.get(mRequests.size() - 2);
+ mListener.onStatusChanged(prevRequest, STATUS_SUSPENDED);
+ }
+ }
+
+ /**
+ * Cancels the request with the specified {@code token} and notifies the listener of all changes
+ * to request status as a result of this operation.
+ */
+ void cancelRequest(@NonNull IBinder token) {
+ int index = getRequestIndex(token);
+ if (index == -1) {
+ return;
+ }
+
+ OverrideRequest request = mRequests.remove(index);
+ if (index == mRequests.size() && mRequests.size() > 0) {
+ // We removed the current active request so we need to set the new active request
+ // before cancelling this request.
+ OverrideRequest newTop = getLast(mRequests);
+ mListener.onStatusChanged(newTop, STATUS_ACTIVE);
+ }
+ mListener.onStatusChanged(request, STATUS_CANCELED);
+ }
+
+ /**
+ * Returns {@code true} if this controller is current managing a request with the specified
+ * {@code token}, {@code false} otherwise.
+ */
+ boolean hasRequest(@NonNull IBinder token) {
+ return getRequestIndex(token) != -1;
+ }
+
+ /**
+ * Notifies the controller that the process with the specified {@code pid} has died. The
+ * controller will notify the listener of all changes to request status as a result of this
+ * operation.
+ */
+ void handleProcessDied(int pid) {
+ if (mRequests.isEmpty()) {
+ return;
+ }
+
+ OverrideRequest prevActiveRequest = getLast(mRequests);
+ for (OverrideRequest request : mRequests) {
+ if (request.getPid() == pid) {
+ mTmpRequestsToCancel.add(request);
+ }
+ }
+
+ mRequests.removeAll(mTmpRequestsToCancel);
+ if (!mRequests.isEmpty()) {
+ OverrideRequest newActiveRequest = getLast(mRequests);
+ if (newActiveRequest != prevActiveRequest) {
+ mListener.onStatusChanged(newActiveRequest, STATUS_ACTIVE);
+ }
+ }
+
+ for (int i = 0; i < mTmpRequestsToCancel.size(); i++) {
+ mListener.onStatusChanged(mTmpRequestsToCancel.get(i), STATUS_CANCELED);
+ }
+ mTmpRequestsToCancel.clear();
+ }
+
+ /**
+ * Notifies the controller that the base state has changed. The controller will notify the
+ * listener of all changes to request status as a result of this change.
+ *
+ * @return {@code true} if calling this method has lead to a new active request, {@code false}
+ * otherwise.
+ */
+ boolean handleBaseStateChanged() {
+ if (mRequests.isEmpty()) {
+ return false;
+ }
+
+ OverrideRequest prevActiveRequest = getLast(mRequests);
+ for (int i = 0; i < mRequests.size(); i++) {
+ OverrideRequest request = mRequests.get(i);
+ if ((request.getFlags() & DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES) != 0) {
+ mTmpRequestsToCancel.add(request);
+ }
+ }
+
+ mRequests.removeAll(mTmpRequestsToCancel);
+ OverrideRequest newActiveRequest = null;
+ if (!mRequests.isEmpty()) {
+ newActiveRequest = getLast(mRequests);
+ if (newActiveRequest != prevActiveRequest) {
+ mListener.onStatusChanged(newActiveRequest, STATUS_ACTIVE);
+ }
+ }
+
+ for (int i = 0; i < mTmpRequestsToCancel.size(); i++) {
+ mListener.onStatusChanged(mTmpRequestsToCancel.get(i), STATUS_CANCELED);
+ }
+ mTmpRequestsToCancel.clear();
+
+ return newActiveRequest != prevActiveRequest;
+ }
+
+ /**
+ * Notifies the controller that the set of supported states has changed. The controller will
+ * notify the listener of all changes to request status as a result of this change.
+ *
+ * @return {@code true} if calling this method has lead to a new active request, {@code false}
+ * otherwise.
+ */
+ boolean handleNewSupportedStates(int[] newSupportedStates) {
+ if (mRequests.isEmpty()) {
+ return false;
+ }
+
+ OverrideRequest prevActiveRequest = getLast(mRequests);
+ for (int i = 0; i < mRequests.size(); i++) {
+ OverrideRequest request = mRequests.get(i);
+ if (!contains(newSupportedStates, request.getRequestedState())) {
+ mTmpRequestsToCancel.add(request);
+ }
+ }
+
+ mRequests.removeAll(mTmpRequestsToCancel);
+ OverrideRequest newActiveRequest = null;
+ if (!mRequests.isEmpty()) {
+ newActiveRequest = getLast(mRequests);
+ if (newActiveRequest != prevActiveRequest) {
+ mListener.onStatusChanged(newActiveRequest, STATUS_ACTIVE);
+ }
+ }
+
+ for (int i = 0; i < mTmpRequestsToCancel.size(); i++) {
+ mListener.onStatusChanged(mTmpRequestsToCancel.get(i), STATUS_CANCELED);
+ }
+ mTmpRequestsToCancel.clear();
+
+ return newActiveRequest != prevActiveRequest;
+ }
+
+ void dumpInternal(PrintWriter pw) {
+ final int requestCount = mRequests.size();
+ pw.println();
+ pw.println("Override requests: size=" + requestCount);
+ for (int i = 0; i < requestCount; i++) {
+ OverrideRequest overrideRequest = mRequests.get(i);
+ int status = (i == requestCount - 1) ? STATUS_ACTIVE : STATUS_SUSPENDED;
+ pw.println(" " + i + ": mPid=" + overrideRequest.getPid()
+ + ", mRequestedState=" + overrideRequest.getRequestedState()
+ + ", mFlags=" + overrideRequest.getFlags()
+ + ", mStatus=" + statusToString(status));
+ }
+ }
+
+ private int getRequestIndex(@NonNull IBinder token) {
+ final int numberOfRequests = mRequests.size();
+ if (numberOfRequests == 0) {
+ return -1;
+ }
+
+ for (int i = 0; i < numberOfRequests; i++) {
+ OverrideRequest request = mRequests.get(i);
+ if (request.getToken() == token) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Nullable
+ private static T getLast(List list) {
+ return list.size() > 0 ? list.get(list.size() - 1) : null;
+ }
+
+ private static boolean contains(int[] array, int value) {
+ for (int i = 0; i < array.length; i++) {
+ if (array[i] == value) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public interface StatusChangeListener {
+ /**
+ * Notifies the listener of a change in request status. If a change within the controller
+ * causes one request to become active and one to become either suspended or cancelled, this
+ * method is guaranteed to be called with the active request first before the suspended or
+ * cancelled request.
+ */
+ void onStatusChanged(@NonNull OverrideRequest request, @RequestStatus int newStatus);
+ }
+}
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 ff8fbda6c83ec..5ebe8321127c0 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -69,6 +69,17 @@ public final class DeviceStateManagerServiceTest {
mProvider = new TestDeviceStateProvider();
mPolicy = new TestDeviceStatePolicy(mProvider);
mService = new DeviceStateManagerService(InstrumentationRegistry.getContext(), mPolicy);
+ flushHandler(); // Flush the handler to ensure the initial values are committed.
+ }
+
+ private void flushHandler() {
+ flushHandler(1);
+ }
+
+ private void flushHandler(int count) {
+ for (int i = 0; i < count; i++) {
+ mService.getHandler().runWithScissors(() -> {}, 0);
+ }
}
@Test
@@ -80,6 +91,7 @@ public final class DeviceStateManagerServiceTest {
DEFAULT_DEVICE_STATE.getIdentifier());
mProvider.setState(OTHER_DEVICE_STATE.getIdentifier());
+ flushHandler();
assertEquals(mService.getCommittedState(), Optional.of(OTHER_DEVICE_STATE));
assertEquals(mService.getPendingState(), Optional.empty());
assertEquals(mService.getBaseState(), Optional.of(OTHER_DEVICE_STATE));
@@ -92,6 +104,7 @@ public final class DeviceStateManagerServiceTest {
mPolicy.blockConfigure();
mProvider.setState(OTHER_DEVICE_STATE.getIdentifier());
+ flushHandler();
assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
assertEquals(mService.getPendingState(), Optional.of(OTHER_DEVICE_STATE));
assertEquals(mService.getBaseState(), Optional.of(OTHER_DEVICE_STATE));
@@ -99,6 +112,7 @@ public final class DeviceStateManagerServiceTest {
OTHER_DEVICE_STATE.getIdentifier());
mProvider.setState(DEFAULT_DEVICE_STATE.getIdentifier());
+ flushHandler();
assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
assertEquals(mService.getPendingState(), Optional.of(OTHER_DEVICE_STATE));
assertEquals(mService.getBaseState(), Optional.of(DEFAULT_DEVICE_STATE));
@@ -106,6 +120,7 @@ public final class DeviceStateManagerServiceTest {
OTHER_DEVICE_STATE.getIdentifier());
mPolicy.resumeConfigure();
+ flushHandler();
assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
assertEquals(mService.getPendingState(), Optional.empty());
assertEquals(mService.getBaseState(), Optional.of(DEFAULT_DEVICE_STATE));
@@ -149,6 +164,7 @@ public final class DeviceStateManagerServiceTest {
assertEquals(mService.getBaseState(), Optional.of(DEFAULT_DEVICE_STATE));
mProvider.notifySupportedDeviceStates(new DeviceState[]{ DEFAULT_DEVICE_STATE });
+ flushHandler();
// The current committed and requests states do not change because the current state remains
// supported.
@@ -166,6 +182,7 @@ public final class DeviceStateManagerServiceTest {
mService.getBinderService().registerCallback(callback);
// An initial callback will be triggered on registration, so we clear it here.
+ flushHandler();
callback.clearLastNotifiedInfo();
assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
@@ -174,6 +191,7 @@ public final class DeviceStateManagerServiceTest {
mProvider.notifySupportedDeviceStates(new DeviceState[]{ DEFAULT_DEVICE_STATE,
OTHER_DEVICE_STATE });
+ flushHandler();
// The current committed and requests states do not change because the current state remains
// supported.
@@ -203,12 +221,14 @@ public final class DeviceStateManagerServiceTest {
mService.getBinderService().registerCallback(callback);
mProvider.setState(OTHER_DEVICE_STATE.getIdentifier());
+ flushHandler();
assertEquals(callback.getLastNotifiedInfo().baseState,
OTHER_DEVICE_STATE.getIdentifier());
assertEquals(callback.getLastNotifiedInfo().currentState,
OTHER_DEVICE_STATE.getIdentifier());
mProvider.setState(DEFAULT_DEVICE_STATE.getIdentifier());
+ flushHandler();
assertEquals(callback.getLastNotifiedInfo().baseState,
DEFAULT_DEVICE_STATE.getIdentifier());
assertEquals(callback.getLastNotifiedInfo().currentState,
@@ -216,6 +236,7 @@ public final class DeviceStateManagerServiceTest {
mPolicy.blockConfigure();
mProvider.setState(OTHER_DEVICE_STATE.getIdentifier());
+ flushHandler();
// The callback should not have been notified of the state change as the policy is still
// pending callback.
assertEquals(callback.getLastNotifiedInfo().baseState,
@@ -224,6 +245,7 @@ public final class DeviceStateManagerServiceTest {
DEFAULT_DEVICE_STATE.getIdentifier());
mPolicy.resumeConfigure();
+ flushHandler();
// Now that the policy is finished processing the callback should be notified of the state
// change.
assertEquals(callback.getLastNotifiedInfo().baseState,
@@ -236,6 +258,7 @@ public final class DeviceStateManagerServiceTest {
public void registerCallback_emitsInitialValue() throws RemoteException {
TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
+ flushHandler();
assertNotNull(callback.getLastNotifiedInfo());
assertEquals(callback.getLastNotifiedInfo().baseState,
DEFAULT_DEVICE_STATE.getIdentifier());
@@ -247,6 +270,7 @@ public final class DeviceStateManagerServiceTest {
public void requestState() throws RemoteException {
TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
+ flushHandler();
final IBinder token = new Binder();
assertEquals(callback.getLastNotifiedStatus(token),
@@ -254,6 +278,10 @@ public final class DeviceStateManagerServiceTest {
mService.getBinderService().requestState(token, OTHER_DEVICE_STATE.getIdentifier(),
0 /* flags */);
+ // Flush the handler twice. The first flush ensures the request is added and the policy is
+ // notified, while the second flush ensures the callback is notified once the change is
+ // committed.
+ flushHandler(2 /* count */);
assertEquals(callback.getLastNotifiedStatus(token),
TestDeviceStateManagerCallback.STATUS_ACTIVE);
@@ -271,6 +299,7 @@ public final class DeviceStateManagerServiceTest {
OTHER_DEVICE_STATE.getIdentifier());
mService.getBinderService().cancelRequest(token);
+ flushHandler();
assertEquals(callback.getLastNotifiedStatus(token),
TestDeviceStateManagerCallback.STATUS_CANCELED);
@@ -291,6 +320,7 @@ public final class DeviceStateManagerServiceTest {
public void requestState_pendingStateAtRequest() throws RemoteException {
TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
+ flushHandler();
mPolicy.blockConfigure();
@@ -303,6 +333,10 @@ public final class DeviceStateManagerServiceTest {
mService.getBinderService().requestState(firstRequestToken,
OTHER_DEVICE_STATE.getIdentifier(), 0 /* flags */);
+ // Flush the handler twice. The first flush ensures the request is added and the policy is
+ // notified, while the second flush ensures the callback is notified once the change is
+ // committed.
+ flushHandler(2 /* count */);
assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
assertEquals(mService.getPendingState(), Optional.of(OTHER_DEVICE_STATE));
@@ -312,8 +346,8 @@ public final class DeviceStateManagerServiceTest {
mService.getBinderService().requestState(secondRequestToken,
DEFAULT_DEVICE_STATE.getIdentifier(), 0 /* flags */);
-
mPolicy.resumeConfigureOnce();
+ flushHandler();
// First request status is now suspended as there is another pending request.
assertEquals(callback.getLastNotifiedStatus(firstRequestToken),
@@ -330,6 +364,7 @@ public final class DeviceStateManagerServiceTest {
DEFAULT_DEVICE_STATE.getIdentifier());
mPolicy.resumeConfigure();
+ flushHandler();
assertEquals(mService.getCommittedState(), Optional.of(DEFAULT_DEVICE_STATE));
assertEquals(mService.getPendingState(), Optional.empty());
@@ -339,6 +374,7 @@ public final class DeviceStateManagerServiceTest {
// Now cancel the second request to make the first request active.
mService.getBinderService().cancelRequest(secondRequestToken);
+ flushHandler();
assertEquals(callback.getLastNotifiedStatus(firstRequestToken),
TestDeviceStateManagerCallback.STATUS_ACTIVE);
@@ -356,6 +392,7 @@ public final class DeviceStateManagerServiceTest {
public void requestState_sameAsBaseState() throws RemoteException {
TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
+ flushHandler();
final IBinder token = new Binder();
assertEquals(callback.getLastNotifiedStatus(token),
@@ -363,6 +400,7 @@ public final class DeviceStateManagerServiceTest {
mService.getBinderService().requestState(token, DEFAULT_DEVICE_STATE.getIdentifier(),
0 /* flags */);
+ flushHandler();
assertEquals(callback.getLastNotifiedStatus(token),
TestDeviceStateManagerCallback.STATUS_ACTIVE);
@@ -372,6 +410,7 @@ public final class DeviceStateManagerServiceTest {
public void requestState_flagCancelWhenBaseChanges() throws RemoteException {
TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
+ flushHandler();
final IBinder token = new Binder();
assertEquals(callback.getLastNotifiedStatus(token),
@@ -379,6 +418,10 @@ public final class DeviceStateManagerServiceTest {
mService.getBinderService().requestState(token, OTHER_DEVICE_STATE.getIdentifier(),
DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES);
+ // Flush the handler twice. The first flush ensures the request is added and the policy is
+ // notified, while the second flush ensures the callback is notified once the change is
+ // committed.
+ flushHandler(2 /* count */);
assertEquals(callback.getLastNotifiedStatus(token),
TestDeviceStateManagerCallback.STATUS_ACTIVE);
@@ -391,6 +434,7 @@ public final class DeviceStateManagerServiceTest {
OTHER_DEVICE_STATE.getIdentifier());
mProvider.setState(OTHER_DEVICE_STATE.getIdentifier());
+ flushHandler();
// Request is canceled because the base state changed.
assertEquals(callback.getLastNotifiedStatus(token),
@@ -407,6 +451,7 @@ public final class DeviceStateManagerServiceTest {
public void requestState_becomesUnsupported() throws RemoteException {
TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
+ flushHandler();
final IBinder token = new Binder();
assertEquals(callback.getLastNotifiedStatus(token),
@@ -414,6 +459,7 @@ public final class DeviceStateManagerServiceTest {
mService.getBinderService().requestState(token, OTHER_DEVICE_STATE.getIdentifier(),
0 /* flags */);
+ flushHandler();
assertEquals(callback.getLastNotifiedStatus(token),
TestDeviceStateManagerCallback.STATUS_ACTIVE);
@@ -425,6 +471,7 @@ public final class DeviceStateManagerServiceTest {
OTHER_DEVICE_STATE.getIdentifier());
mProvider.notifySupportedDeviceStates(new DeviceState[]{ DEFAULT_DEVICE_STATE });
+ flushHandler();
// Request is canceled because the state is no longer supported.
assertEquals(callback.getLastNotifiedStatus(token),
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
new file mode 100644
index 0000000000000..6f6632fd033f7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
@@ -0,0 +1,204 @@
+/*
+ * 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 com.android.server.devicestate;
+
+import static com.android.server.devicestate.OverrideRequestController.STATUS_ACTIVE;
+import static com.android.server.devicestate.OverrideRequestController.STATUS_CANCELED;
+import static com.android.server.devicestate.OverrideRequestController.STATUS_SUSPENDED;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNull;
+
+import android.annotation.Nullable;
+import android.hardware.devicestate.DeviceStateRequest;
+import android.os.Binder;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link OverrideRequestController}.
+ *
+ * Run with atest OverrideRequestControllerTest.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public final class OverrideRequestControllerTest {
+ private TestStatusChangeListener mStatusListener;
+ private OverrideRequestController mController;
+
+ @Before
+ public void setup() {
+ mStatusListener = new TestStatusChangeListener();
+ mController = new OverrideRequestController(mStatusListener);
+ }
+
+ @Test
+ public void addRequest() {
+ OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* requestedState */, 0 /* flags */);
+ assertNull(mStatusListener.getLastStatus(request));
+
+ mController.addRequest(request);
+ assertEquals(mStatusListener.getLastStatus(request).intValue(), STATUS_ACTIVE);
+ }
+
+ @Test
+ public void addRequest_suspendExistingRequest() {
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* requestedState */, 0 /* flags */);
+ assertNull(mStatusListener.getLastStatus(firstRequest));
+
+ mController.addRequest(firstRequest);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+
+ OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* requestedState */, 0 /* flags */);
+ assertNull(mStatusListener.getLastStatus(secondRequest));
+
+ mController.addRequest(secondRequest);
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+ }
+
+ @Test
+ public void addRequest_cancelActiveRequest() {
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* requestedState */, 0 /* flags */);
+ OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* requestedState */, 0 /* flags */);
+
+ mController.addRequest(firstRequest);
+ mController.addRequest(secondRequest);
+
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+ mController.cancelRequest(secondRequest.getToken());
+
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_CANCELED);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+ }
+
+ @Test
+ public void addRequest_cancelSuspendedRequest() {
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* requestedState */, 0 /* flags */);
+ OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* requestedState */, 0 /* flags */);
+
+ mController.addRequest(firstRequest);
+ mController.addRequest(secondRequest);
+
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+ mController.cancelRequest(firstRequest.getToken());
+
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED);
+ }
+
+ @Test
+ public void handleBaseStateChanged() {
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* requestedState */, 0 /* flags */);
+ OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* requestedState */,
+ DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES /* flags */);
+
+ mController.addRequest(firstRequest);
+ mController.addRequest(secondRequest);
+
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+ mController.handleBaseStateChanged();
+
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_CANCELED);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+ }
+
+ @Test
+ public void handleProcessDied() {
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 0 /* requestedState */, 0 /* flags */);
+ OverrideRequest secondRequest = new OverrideRequest(new Binder(), 1 /* pid */,
+ 0 /* requestedState */, 0 /* flags */);
+
+ mController.addRequest(firstRequest);
+ mController.addRequest(secondRequest);
+
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+ mController.handleProcessDied(1);
+
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_CANCELED);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+
+ mController.handleProcessDied(0);
+
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED);
+ }
+
+ @Test
+ public void handleNewSupportedStates() {
+ OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 1 /* requestedState */, 0 /* flags */);
+ OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+ 2 /* requestedState */, 0 /* flags */);
+
+ mController.addRequest(firstRequest);
+ mController.addRequest(secondRequest);
+
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_ACTIVE);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_SUSPENDED);
+
+ mController.handleNewSupportedStates(new int[]{ 0, 1 });
+
+ assertEquals(mStatusListener.getLastStatus(secondRequest).intValue(), STATUS_CANCELED);
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
+
+ mController.handleNewSupportedStates(new int[]{ 0 });
+
+ assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED);
+ }
+
+ private static final class TestStatusChangeListener implements
+ OverrideRequestController.StatusChangeListener {
+ private Map mLastStatusMap = new HashMap<>();
+
+ @Override
+ public void onStatusChanged(@NonNull OverrideRequest request, int newStatus) {
+ mLastStatusMap.put(request, newStatus);
+ }
+
+ @Nullable
+ public Integer getLastStatus(OverrideRequest request) {
+ return mLastStatusMap.get(request);
+ }
+ }
+}