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

Commit c26defea authored by Darryl L Johnson's avatar Darryl L Johnson
Browse files

Add device state request APIs to DeviceStateManager.

This change promotes DeviceStateManager to a TestApi and exposes three
additional methods:
- getSupportedStates: returns a list of state ids that can be used with
requestState().
- submitRequest: requests that the device enter the supplied state.
- cancelRequest: clears the current requested device state.
to be used initially in CTS tests.

Bug: 177235528
Bug: 177236115
Test: atest DeviceStateManagerServiceTest
Test: atest DeviceStateManagerGlobalTest

Change-Id: Ieae9208420ddecea641a5c44f1610d3cb7071b07
parent 951b54e2
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ package android {
    field public static final String CLEAR_APP_USER_DATA = "android.permission.CLEAR_APP_USER_DATA";
    field public static final String CONFIGURE_DISPLAY_BRIGHTNESS = "android.permission.CONFIGURE_DISPLAY_BRIGHTNESS";
    field public static final String CONTROL_DEVICE_LIGHTS = "android.permission.CONTROL_DEVICE_LIGHTS";
    field public static final String CONTROL_DEVICE_STATE = "android.permission.CONTROL_DEVICE_STATE";
    field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES";
    field @Deprecated public static final String MANAGE_ACTIVITY_STACKS = "android.permission.MANAGE_ACTIVITY_STACKS";
    field public static final String MANAGE_ACTIVITY_TASKS = "android.permission.MANAGE_ACTIVITY_TASKS";
@@ -895,6 +896,40 @@ package android.hardware.camera2 {

}

package android.hardware.devicestate {

  public final class DeviceStateManager {
    method public void addDeviceStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.devicestate.DeviceStateManager.DeviceStateListener);
    method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) public void cancelRequest(@NonNull android.hardware.devicestate.DeviceStateRequest);
    method @NonNull public int[] getSupportedStates();
    method public void removeDeviceStateListener(@NonNull android.hardware.devicestate.DeviceStateManager.DeviceStateListener);
    method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE) public void requestState(@NonNull android.hardware.devicestate.DeviceStateRequest, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.devicestate.DeviceStateRequest.Callback);
  }

  public static interface DeviceStateManager.DeviceStateListener {
    method public void onDeviceStateChanged(int);
  }

  public final class DeviceStateRequest {
    method public int getFlags();
    method public int getState();
    method @NonNull public static android.hardware.devicestate.DeviceStateRequest.Builder newBuilder(int);
    field public static final int FLAG_CANCEL_WHEN_BASE_CHANGES = 1; // 0x1
  }

  public static final class DeviceStateRequest.Builder {
    method @NonNull public android.hardware.devicestate.DeviceStateRequest build();
    method @NonNull public android.hardware.devicestate.DeviceStateRequest.Builder setFlags(int);
  }

  public static interface DeviceStateRequest.Callback {
    method public default void onRequestActivated(@NonNull android.hardware.devicestate.DeviceStateRequest);
    method public default void onRequestCanceled(@NonNull android.hardware.devicestate.DeviceStateRequest);
    method public default void onRequestSuspended(@NonNull android.hardware.devicestate.DeviceStateRequest);
  }

}

package android.hardware.display {

  public class AmbientDisplayConfiguration {
+67 −7
Original line number Diff line number Diff line
@@ -16,8 +16,12 @@

package android.hardware.devicestate;

import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.content.Context;

import java.util.concurrent.Executor;
@@ -28,13 +32,19 @@ import java.util.concurrent.Executor;
 *
 * @hide
 */
@TestApi
@SystemService(Context.DEVICE_STATE_SERVICE)
public final class DeviceStateManager {
    /** Invalid device state. */
    /**
     * Invalid device state.
     *
     * @hide
     */
    public static final int INVALID_DEVICE_STATE = -1;

    private DeviceStateManagerGlobal mGlobal;
    private final DeviceStateManagerGlobal mGlobal;

    /** @hide */
    public DeviceStateManager() {
        DeviceStateManagerGlobal global = DeviceStateManagerGlobal.getInstance();
        if (global == null) {
@@ -44,24 +54,74 @@ public final class DeviceStateManager {
        mGlobal = global;
    }

    /**
     * Returns the list of device states that are supported and can be requested with
     * {@link #requestState(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}.
     */
    @NonNull
    public int[] getSupportedStates() {
        return mGlobal.getSupportedStates();
    }

    /**
     * Submits a {@link DeviceStateRequest request} to modify the device state.
     * <p>
     * By default, the request is kept active until a call to
     * {@link #cancelRequest(DeviceStateRequest)} or until one of the following occurs:
     * <ul>
     *     <li>Another processes submits a request succeeding this request in which case the request
     *     will be suspended until the interrupting request is canceled.
     *     <li>The requested state has become unsupported.
     *     <li>The process submitting the request dies.
     * </ul>
     * However, this behavior can be changed by setting flags on the {@link DeviceStateRequest}.
     *
     * @throws IllegalArgumentException if the requested state is unsupported.
     * @throws SecurityException if the {@link android.Manifest.permission#CONTROL_DEVICE_STATE}
     * permission is not held.
     *
     * @see DeviceStateRequest
     */
    @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)
    public void requestState(@NonNull DeviceStateRequest request,
            @Nullable @CallbackExecutor Executor executor,
            @Nullable DeviceStateRequest.Callback callback) {
        mGlobal.requestState(request, callback, executor);
    }

    /**
     * Cancels a {@link DeviceStateRequest request} previously submitted with a call to
     * {@link #requestState(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}.
     * <p>
     * This method is noop if the {@code request} has not been submitted with a call to
     * {@link #requestState(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}.
     *
     * @throws SecurityException if the {@link android.Manifest.permission#CONTROL_DEVICE_STATE}
     * permission is not held.
     */
    @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)
    public void cancelRequest(@NonNull DeviceStateRequest request) {
        mGlobal.cancelRequest(request);
    }

    /**
     * Registers a listener to receive notifications about changes in device state.
     *
     * @param listener the listener to register.
     * @param executor the executor to process notifications.
     * @param listener the listener to register.
     *
     * @see DeviceStateListener
     */
    public void registerDeviceStateListener(@NonNull DeviceStateListener listener,
            @NonNull Executor executor) {
    public void addDeviceStateListener(@NonNull @CallbackExecutor Executor executor,
            @NonNull DeviceStateListener listener) {
        mGlobal.registerDeviceStateListener(listener, executor);
    }

    /**
     * Unregisters a listener previously registered with
     * {@link #registerDeviceStateListener(DeviceStateListener, Executor)}.
     * {@link #addDeviceStateListener(Executor, DeviceStateListener)}.
     */
    public void unregisterDeviceStateListener(@NonNull DeviceStateListener listener) {
    public void removeDeviceStateListener(@NonNull DeviceStateListener listener) {
        mGlobal.unregisterDeviceStateListener(listener);
    }

+189 −2
Original line number Diff line number Diff line
@@ -20,9 +20,11 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager.DeviceStateListener;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.ArrayMap;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -67,6 +69,9 @@ public final class DeviceStateManagerGlobal {

    @GuardedBy("mLock")
    private final ArrayList<DeviceStateListenerWrapper> mListeners = new ArrayList<>();
    @GuardedBy("mLock")
    private final ArrayMap<IBinder, DeviceStateRequestWrapper> mRequests = new ArrayMap<>();

    @Nullable
    @GuardedBy("mLock")
    private Integer mLastReceivedState;
@@ -76,10 +81,85 @@ public final class DeviceStateManagerGlobal {
        mDeviceStateManager = deviceStateManager;
    }

    /**
     * Returns the set of supported device states.
     *
     * @see DeviceStateManager#getSupportedStates()
     */
    public int[] getSupportedStates() {
        try {
            return mDeviceStateManager.getSupportedDeviceStates();
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

    /**
     * Submits a {@link DeviceStateRequest request} to modify the device state.
     *
     * @see DeviceStateManager#requestState(DeviceStateRequest,
     * Executor, DeviceStateRequest.Callback)
     * @see DeviceStateRequest
     */
    public void requestState(@NonNull DeviceStateRequest request,
            @Nullable DeviceStateRequest.Callback callback, @Nullable Executor executor) {
        if (callback == null && executor != null) {
            throw new IllegalArgumentException("Callback must be supplied with executor.");
        } else if (executor == null && callback != null) {
            throw new IllegalArgumentException("Executor must be supplied with callback.");
        }

        synchronized (mLock) {
            registerCallbackIfNeededLocked();

            if (findRequestTokenLocked(request) != null) {
                // This request has already been submitted.
                return;
            }

            // Add the request wrapper to the mRequests array before requesting the state as the
            // callback could be triggered immediately if the mDeviceStateManager IBinder is in the
            // same process as this instance.
            IBinder token = new Binder();
            mRequests.put(token, new DeviceStateRequestWrapper(request, callback, executor));

            try {
                mDeviceStateManager.requestState(token, request.getState(), request.getFlags());
            } catch (RemoteException ex) {
                mRequests.remove(token);
                throw ex.rethrowFromSystemServer();
            }
        }
    }

    /**
     * Cancels a {@link DeviceStateRequest request} previously submitted with a call to
     * {@link #requestState(DeviceStateRequest, DeviceStateRequest.Callback, Executor)}.
     *
     * @see DeviceStateManager#cancelRequest(DeviceStateRequest)
     */
    public void cancelRequest(@NonNull DeviceStateRequest request) {
        synchronized (mLock) {
            registerCallbackIfNeededLocked();

            final IBinder token = findRequestTokenLocked(request);
            if (token == null) {
                // This request has not been submitted.
                return;
            }

            try {
                mDeviceStateManager.cancelRequest(token);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
        }
    }

    /**
     * Registers a listener to receive notifications about changes in device state.
     *
     * @see DeviceStateManager#registerDeviceStateListener(DeviceStateListener, Executor)
     * @see DeviceStateManager#addDeviceStateListener(Executor, DeviceStateListener)
     */
    @VisibleForTesting(visibility = Visibility.PACKAGE)
    public void registerDeviceStateListener(@NonNull DeviceStateListener listener,
@@ -112,7 +192,7 @@ public final class DeviceStateManagerGlobal {
     * Unregisters a listener previously registered with
     * {@link #registerDeviceStateListener(DeviceStateListener, Executor)}.
     *
     * @see DeviceStateManager#registerDeviceStateListener(DeviceStateListener, Executor)
     * @see DeviceStateManager#addDeviceStateListener(Executor, DeviceStateListener)
     */
    @VisibleForTesting(visibility = Visibility.PACKAGE)
    public void unregisterDeviceStateListener(DeviceStateListener listener) {
@@ -144,6 +224,17 @@ public final class DeviceStateManagerGlobal {
        return -1;
    }

    @Nullable
    private IBinder findRequestTokenLocked(@NonNull DeviceStateRequest request) {
        for (int i = 0; i < mRequests.size(); i++) {
            if (mRequests.valueAt(i).mRequest.equals(request)) {
                return mRequests.keyAt(i);
            }
        }
        return null;
    }

    /** Handles a call from the server that the device state has changed. */
    private void handleDeviceStateChanged(int newDeviceState) {
        ArrayList<DeviceStateListenerWrapper> listeners;
        synchronized (mLock) {
@@ -156,11 +247,68 @@ public final class DeviceStateManagerGlobal {
        }
    }

    /**
     * Handles a call from the server that a request for the supplied {@code token} has become
     * active.
     */
    private void handleRequestActive(IBinder token) {
        DeviceStateRequestWrapper request;
        synchronized (mLock) {
            request = mRequests.get(token);
        }
        if (request != null) {
            request.notifyRequestActive();
        }
    }

    /**
     * Handles a call from the server that a request for the supplied {@code token} has become
     * suspended.
     */
    private void handleRequestSuspended(IBinder token) {
        DeviceStateRequestWrapper request;
        synchronized (mLock) {
            request = mRequests.get(token);
        }
        if (request != null) {
            request.notifyRequestSuspended();
        }
    }

    /**
     * Handles a call from the server that a request for the supplied {@code token} has become
     * canceled.
     */
    private void handleRequestCanceled(IBinder token) {
        DeviceStateRequestWrapper request;
        synchronized (mLock) {
            request = mRequests.remove(token);
        }
        if (request != null) {
            request.notifyRequestCanceled();
        }
    }

    private final class DeviceStateManagerCallback extends IDeviceStateManagerCallback.Stub {
        @Override
        public void onDeviceStateChanged(int deviceState) {
            handleDeviceStateChanged(deviceState);
        }

        @Override
        public void onRequestActive(IBinder token) {
            handleRequestActive(token);
        }

        @Override
        public void onRequestSuspended(IBinder token) {
            handleRequestSuspended(token);
        }

        @Override
        public void onRequestCanceled(IBinder token) {
            handleRequestCanceled(token);
        }
    }

    private static final class DeviceStateListenerWrapper {
@@ -176,4 +324,43 @@ public final class DeviceStateManagerGlobal {
            mExecutor.execute(() -> mDeviceStateListener.onDeviceStateChanged(newDeviceState));
        }
    }

    private static final class DeviceStateRequestWrapper {
        private final DeviceStateRequest mRequest;
        @Nullable
        private final DeviceStateRequest.Callback mCallback;
        @Nullable
        private final Executor mExecutor;

        DeviceStateRequestWrapper(@NonNull DeviceStateRequest request,
                @Nullable DeviceStateRequest.Callback callback, @Nullable Executor executor) {
            mRequest = request;
            mCallback = callback;
            mExecutor = executor;
        }

        void notifyRequestActive() {
            if (mCallback == null) {
                return;
            }

            mExecutor.execute(() -> mCallback.onRequestActivated(mRequest));
        }

        void notifyRequestSuspended() {
            if (mCallback == null) {
                return;
            }

            mExecutor.execute(() -> mCallback.onRequestSuspended(mRequest));
        }

        void notifyRequestCanceled() {
            if (mCallback == null) {
                return;
            }

            mExecutor.execute(() -> mCallback.onRequestSuspended(mRequest));
        }
    }
}
+163 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.hardware.devicestate;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.TestApi;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;

/**
 * A request to alter the state of the device managed by {@link DeviceStateManager}.
 * <p>
 * Once constructed, a {@link DeviceStateRequest request} can be submitted with a call to
 * {@link DeviceStateManager#requestState(DeviceStateRequest, Executor,
 * DeviceStateRequest.Callback)}.
 * <p>
 * By default, the request is kept active until a call to
 * {@link DeviceStateManager#cancelRequest(DeviceStateRequest)} or until one of the following
 * occurs:
 * <ul>
 *     <li>Another processes submits a request succeeding this request in which case the request
 *     will be suspended until the interrupting request is canceled.
 *     <li>The requested state has become unsupported.
 *     <li>The process submitting the request dies.
 * </ul>
 * However, this behavior can be changed by setting flags on the request. For example, the
 * {@link #FLAG_CANCEL_WHEN_BASE_CHANGES} flag will extend this behavior to also cancel the
 * request whenever the base (non-override) device state changes.
 *
 * @see DeviceStateManager
 *
 * @hide
 */
@TestApi
public final class DeviceStateRequest {
    /**
     * Flag that indicates the request should be canceled automatically when the base
     * (non-override) device state changes. Useful when the requestor only wants the request to
     * remain active while the base state remains constant and automatically cancel when the user
     * manipulates the device into a different state.
     */
    public static final int FLAG_CANCEL_WHEN_BASE_CHANGES = 1 << 0;

    /** @hide */
    @IntDef(prefix = {"FLAG_"}, flag = true, value = {
            FLAG_CANCEL_WHEN_BASE_CHANGES,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface RequestFlags {}

    /**
     * Creates a new {@link Builder} for a {@link DeviceStateRequest}. Must be one of the supported
     * states for the device which can be queried with a call to
     * {@link DeviceStateManager#getSupportedStates()}.
     *
     * @param requestedState the device state being requested.
     */
    @NonNull
    public static Builder newBuilder(int requestedState) {
        return new Builder(requestedState);
    }

    /**
     * Builder for {@link DeviceStateRequest}. An instance can be obtained through
     * {@link #newBuilder(int)}.
     */
    public static final class Builder {
        private final int mRequestedState;
        private int mFlags;

        private Builder(int requestedState) {
            mRequestedState = requestedState;
        }

        /**
         * Sets the flag bits provided within {@code flags} with all other bits remaining
         * unchanged.
         */
        @NonNull
        public Builder setFlags(@RequestFlags int flags) {
            mFlags |= flags;
            return this;
        }

        /**
         * Returns a new {@link DeviceStateRequest} object whose state matches the state set on the
         * builder.
         */
        @NonNull
        public DeviceStateRequest build() {
            return new DeviceStateRequest(mRequestedState, mFlags);
        }
    }

    /** Callback to track the status of a request. */
    public interface Callback {
        /**
         * Called to indicate the request has become active and the device state will match the
         * requested state.
         * <p>
         * Guaranteed to be called after a call to
         * {@link DeviceStateManager.DeviceStateListener#onDeviceStateChanged(int)} with a state
         * matching the requested state.
         */
        default void onRequestActivated(@NonNull DeviceStateRequest request) {}

        /**
         * Called to indicate the request has been temporarily suspended.
         * <p>
         * Guaranteed to be called before a call to
         * {@link DeviceStateManager.DeviceStateListener#onDeviceStateChanged(int)}.
         */
        default void onRequestSuspended(@NonNull DeviceStateRequest request) {}

        /**
         * Called to indicate the request has been canceled. The request can be resubmitted with
         * another call to {@link DeviceStateManager#requestState(DeviceStateRequest, Executor,
         * DeviceStateRequest.Callback)}.
         * <p>
         * Guaranteed to be called before a call to
         * {@link DeviceStateManager.DeviceStateListener#onDeviceStateChanged(int)}.
         * <p>
         * Note: A call to {@link #onRequestSuspended(DeviceStateRequest)} is not guaranteed to
         * occur before this method.
         */
        default void onRequestCanceled(@NonNull DeviceStateRequest request) {}
    }

    private final int mRequestedState;
    @RequestFlags
    private final int mFlags;

    private DeviceStateRequest(int requestedState, @RequestFlags int flags) {
        mRequestedState = requestedState;
        mFlags = flags;
    }

    public int getState() {
        return mRequestedState;
    }

    @RequestFlags
    public int getFlags() {
        return mFlags;
    }
}
+40 −0
Original line number Diff line number Diff line
@@ -20,5 +20,45 @@ import android.hardware.devicestate.IDeviceStateManagerCallback;

/** @hide */
interface IDeviceStateManager {
    /**
     * Registers a callback to receive notifications from the device state manager. Only one
     * callback can be registered per-process.
     * <p>
     * As the callback mechanism is used to alert the caller of changes to request status a callback
     * <b>MUST</b> be registered before calling {@link #requestState(IBinder, int, int)} or
     * {@link #cancelRequest(IBinder)}. Otherwise an exception will be thrown.
     *
     * @throws SecurityException if a callback is already registered for the calling process.
     */
    void registerCallback(in IDeviceStateManagerCallback callback);

    /** Returns the array of supported device state identifiers. */
    int[] getSupportedDeviceStates();

    /**
     * Requests that the device enter the supplied {@code state}. A callback <b>MUST</b> have been
     * previously registered with {@link #registerCallback(IDeviceStateManagerCallback)} before a
     * call to this method.
     *
     * @param token the request token previously registered with
     *        {@link #requestState(IBinder, int, int)}
     *
     * @throws IllegalStateException if a callback has not yet been registered for the calling
     *         process.
     * @throws IllegalStateException if the supplied {@code token} has already been registered.
     * @throws IllegalArgumentException if the supplied {@code state} is not supported.
     */
    void requestState(IBinder token, int state, int flags);

    /**
     * Cancels a request previously submitted with a call to
     * {@link #requestState(IBinder, int, int)}.
     *
     * @param token the request token previously registered with
     *        {@link #requestState(IBinder, int, int)}
     *
     * @throws IllegalStateException if the supplied {@code token} has not been previously
     *         requested.
     */
    void cancelRequest(IBinder token);
}
Loading