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); + } + } +}