blob: 6db5aee106a85c8c825f8c611e1414c91dfdb557 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.hardware.devicestate;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.ActivityThread;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.Trace;
import android.util.ArrayMap;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.internal.util.ArrayUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* Provides communication with the device state system service on behalf of applications.
*
* @see DeviceStateManager
*
* @hide
*/
@VisibleForTesting(visibility = Visibility.PACKAGE)
public final class DeviceStateManagerGlobal {
private static DeviceStateManagerGlobal sInstance;
private static final String TAG = "DeviceStateManagerGlobal";
private static final boolean DEBUG = Build.IS_DEBUGGABLE;
// TODO(b/325124054): Remove when system server refactor is completed
private static int[] sFoldedDeviceStates = new int[0];
/**
* Returns an instance of {@link DeviceStateManagerGlobal}. May return {@code null} if a
* connection with the device state service couldn't be established.
*/
@Nullable
public static DeviceStateManagerGlobal getInstance() {
// TODO(b/325124054): Remove when system server refactor is completed
instantiateFoldedStateArray();
synchronized (DeviceStateManagerGlobal.class) {
if (sInstance == null) {
IBinder b = ServiceManager.getService(Context.DEVICE_STATE_SERVICE);
if (b != null) {
sInstance = new DeviceStateManagerGlobal(IDeviceStateManager
.Stub.asInterface(b));
}
}
return sInstance;
}
}
// TODO(b/325124054): Remove when system server refactor is completed
// TODO(b/325330654): Investigate if we need a Context passed in to DSMGlobal
private static void instantiateFoldedStateArray() {
Context context = ActivityThread.currentApplication();
if (context != null) {
sFoldedDeviceStates = context.getResources().getIntArray(
com.android.internal.R.array.config_foldedDeviceStates);
}
}
private final Object mLock = new Object();
@NonNull
private final IDeviceStateManager mDeviceStateManager;
@Nullable
private DeviceStateManagerCallback mCallback;
@GuardedBy("mLock")
private final ArrayList<DeviceStateCallbackWrapper> mCallbacks = new ArrayList<>();
@GuardedBy("mLock")
private final ArrayMap<IBinder, DeviceStateRequestWrapper> mRequests = new ArrayMap<>();
@Nullable
@GuardedBy("mLock")
private DeviceStateInfo mLastReceivedInfo;
@VisibleForTesting
public DeviceStateManagerGlobal(@NonNull IDeviceStateManager deviceStateManager) {
mDeviceStateManager = deviceStateManager;
registerCallbackIfNeededLocked();
}
/**
* Returns the set of supported device states.
*
* @see DeviceStateManager#getSupportedStates()
*/
public int[] getSupportedStates() {
synchronized (mLock) {
final DeviceStateInfo currentInfo;
if (mLastReceivedInfo != null) {
// If we have mLastReceivedInfo a callback is registered for this instance and it
// is receiving the most recent info from the server. Use that info here.
currentInfo = mLastReceivedInfo;
} else {
// If mLastReceivedInfo is null there is no registered callback so we manually
// fetch the current info.
try {
currentInfo = mDeviceStateManager.getDeviceStateInfo();
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
return Arrays.copyOf(currentInfo.supportedStates, currentInfo.supportedStates.length);
}
}
/**
* Returns the {@link List} of supported device states.
*
* @see DeviceStateManager#getSupportedDeviceStates()
*/
public List<DeviceState> getSupportedDeviceStates() {
synchronized (mLock) {
final DeviceStateInfo currentInfo;
if (mLastReceivedInfo != null) {
// If we have mLastReceivedInfo a callback is registered for this instance and it
// is receiving the most recent info from the server. Use that info here.
currentInfo = mLastReceivedInfo;
} else {
// If mLastReceivedInfo is null there is no registered callback so we manually
// fetch the current info.
try {
currentInfo = mDeviceStateManager.getDeviceStateInfo();
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
return createDeviceStateList(currentInfo.supportedStates);
}
}
/**
* Submits a {@link DeviceStateRequest request} to modify the device state.
*
* @see DeviceStateManager#requestState(DeviceStateRequest, Executor,
* DeviceStateRequest.Callback)
* @see DeviceStateRequest
*/
@RequiresPermission(value = android.Manifest.permission.CONTROL_DEVICE_STATE,
conditional = true)
public void requestState(@NonNull DeviceStateRequest request,
@Nullable Executor executor, @Nullable DeviceStateRequest.Callback callback) {
DeviceStateRequestWrapper requestWrapper = new DeviceStateRequestWrapper(request, callback,
executor);
synchronized (mLock) {
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, requestWrapper);
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, Executor, DeviceStateRequest.Callback)}.
*
* @see DeviceStateManager#cancelStateRequest
*/
@RequiresPermission(value = android.Manifest.permission.CONTROL_DEVICE_STATE,
conditional = true)
public void cancelStateRequest() {
synchronized (mLock) {
try {
mDeviceStateManager.cancelStateRequest();
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}
/**
* Submits a {@link DeviceStateRequest request} to modify the base state of the device.
*
* @see DeviceStateManager#requestBaseStateOverride(DeviceStateRequest, Executor,
* DeviceStateRequest.Callback)
* @see DeviceStateRequest
*/
@RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)
public void requestBaseStateOverride(@NonNull DeviceStateRequest request,
@Nullable Executor executor, @Nullable DeviceStateRequest.Callback callback) {
DeviceStateRequestWrapper requestWrapper = new DeviceStateRequestWrapper(request, callback,
executor);
synchronized (mLock) {
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, requestWrapper);
try {
mDeviceStateManager.requestBaseStateOverride(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 #requestBaseStateOverride(DeviceStateRequest, Executor, DeviceStateRequest.Callback)}.
*
* @see DeviceStateManager#cancelBaseStateOverride
*/
@RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)
public void cancelBaseStateOverride() {
synchronized (mLock) {
try {
mDeviceStateManager.cancelBaseStateOverride();
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}
/**
* Registers a callback to receive notifications about changes in device state.
*
* @see DeviceStateManager#registerCallback(Executor, DeviceStateCallback)
*/
@VisibleForTesting(visibility = Visibility.PACKAGE)
public void registerDeviceStateCallback(@NonNull DeviceStateCallback callback,
@NonNull Executor executor) {
synchronized (mLock) {
int index = findCallbackLocked(callback);
if (index != -1) {
// This callback is already registered.
return;
}
// Add the callback wrapper to the mCallbacks array after registering the callback as
// the callback could be triggered immediately if the mDeviceStateManager IBinder is in
// the same process as this instance.
DeviceStateCallbackWrapper wrapper = new DeviceStateCallbackWrapper(callback, executor);
mCallbacks.add(wrapper);
if (mLastReceivedInfo != null) {
// Copy the array to prevent the callback from modifying the internal state.
final int[] supportedStates = Arrays.copyOf(mLastReceivedInfo.supportedStates,
mLastReceivedInfo.supportedStates.length);
wrapper.notifySupportedStatesChanged(supportedStates);
wrapper.notifySupportedDeviceStatesChanged(createDeviceStateList(supportedStates));
wrapper.notifyBaseStateChanged(mLastReceivedInfo.baseState);
wrapper.notifyStateChanged(mLastReceivedInfo.currentState);
wrapper.notifyDeviceStateChanged(createDeviceState(mLastReceivedInfo.currentState));
}
}
}
/**
* Unregisters a callback previously registered with
* {@link #registerDeviceStateCallback(DeviceStateCallback, Executor)}}.
*
* @see DeviceStateManager#unregisterCallback(DeviceStateCallback)
*/
@VisibleForTesting(visibility = Visibility.PACKAGE)
public void unregisterDeviceStateCallback(@NonNull DeviceStateCallback callback) {
synchronized (mLock) {
int indexToRemove = findCallbackLocked(callback);
if (indexToRemove != -1) {
mCallbacks.remove(indexToRemove);
}
}
}
/**
* Provides notification to the system server that a device state feature overlay
* was dismissed. This should only be called from the {@link android.app.Activity} that
* was showing the overlay corresponding to the feature.
*
* Validation of there being an overlay visible and pending state request is handled on the
* system server.
*/
public void onStateRequestOverlayDismissed(boolean shouldCancelRequest) {
try {
mDeviceStateManager.onStateRequestOverlayDismissed(shouldCancelRequest);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
private void registerCallbackIfNeededLocked() {
if (mCallback == null) {
mCallback = new DeviceStateManagerCallback();
try {
mDeviceStateManager.registerCallback(mCallback);
} catch (RemoteException ex) {
mCallback = null;
throw ex.rethrowFromSystemServer();
}
}
}
private int findCallbackLocked(DeviceStateCallback callback) {
for (int i = 0; i < mCallbacks.size(); i++) {
if (mCallbacks.get(i).mDeviceStateCallback.equals(callback)) {
return i;
}
}
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 info has changed. */
private void handleDeviceStateInfoChanged(@NonNull DeviceStateInfo info) {
ArrayList<DeviceStateCallbackWrapper> callbacks;
DeviceStateInfo oldInfo;
synchronized (mLock) {
oldInfo = mLastReceivedInfo;
mLastReceivedInfo = info;
callbacks = new ArrayList<>(mCallbacks);
}
final int diff = oldInfo == null ? ~0 : info.diff(oldInfo);
if ((diff & DeviceStateInfo.CHANGED_SUPPORTED_STATES) > 0) {
for (int i = 0; i < callbacks.size(); i++) {
// Copy the array to prevent callbacks from modifying the internal state.
final int[] supportedStates = Arrays.copyOf(info.supportedStates,
info.supportedStates.length);
callbacks.get(i).notifySupportedStatesChanged(supportedStates);
callbacks.get(i).notifySupportedDeviceStatesChanged(
createDeviceStateList(supportedStates));
}
}
if ((diff & DeviceStateInfo.CHANGED_BASE_STATE) > 0) {
for (int i = 0; i < callbacks.size(); i++) {
callbacks.get(i).notifyBaseStateChanged(info.baseState);
}
}
if ((diff & DeviceStateInfo.CHANGED_CURRENT_STATE) > 0) {
for (int i = 0; i < callbacks.size(); i++) {
callbacks.get(i).notifyStateChanged(info.currentState);
callbacks.get(i).notifyDeviceStateChanged(createDeviceState(info.currentState));
}
}
}
/**
* 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
* canceled.
*/
private void handleRequestCanceled(IBinder token) {
DeviceStateRequestWrapper request;
synchronized (mLock) {
request = mRequests.remove(token);
}
if (request != null) {
request.notifyRequestCanceled();
}
}
/**
* Creates a {@link DeviceState} object from a device state identifier, with the
* {@link DeviceState} property that corresponds to what display is primary.
*
*/
// TODO(b/325124054): Remove when system server refactor is completed
@NonNull
private DeviceState createDeviceState(int stateIdentifier) {
final Set<@DeviceState.DeviceStateProperties Integer> properties = new HashSet<>();
if (ArrayUtils.contains(sFoldedDeviceStates, stateIdentifier)) {
properties.add(DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY);
} else {
properties.add(DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY);
}
return new DeviceState(stateIdentifier, "" /* name */, properties);
}
/**
* Creates a list of {@link DeviceState} objects from an array of state identifiers.
*/
// TODO(b/325124054): Remove when system server refactor is completed
@NonNull
private List<DeviceState> createDeviceStateList(int[] supportedStates) {
List<DeviceState> deviceStateList = new ArrayList<>();
for (int i = 0; i < supportedStates.length; i++) {
deviceStateList.add(createDeviceState(supportedStates[i]));
}
return deviceStateList;
}
private final class DeviceStateManagerCallback extends IDeviceStateManagerCallback.Stub {
@Override
public void onDeviceStateInfoChanged(DeviceStateInfo info) {
handleDeviceStateInfoChanged(info);
}
@Override
public void onRequestActive(IBinder token) {
handleRequestActive(token);
}
@Override
public void onRequestCanceled(IBinder token) {
handleRequestCanceled(token);
}
}
private static final class DeviceStateCallbackWrapper {
@NonNull
private final DeviceStateCallback mDeviceStateCallback;
@NonNull
private final Executor mExecutor;
DeviceStateCallbackWrapper(@NonNull DeviceStateCallback callback,
@NonNull Executor executor) {
mDeviceStateCallback = callback;
mExecutor = executor;
}
void notifySupportedStatesChanged(int[] newSupportedStates) {
mExecutor.execute(() ->
mDeviceStateCallback.onSupportedStatesChanged(newSupportedStates));
}
void notifySupportedDeviceStatesChanged(List<DeviceState> newSupportedDeviceStates) {
mExecutor.execute(() ->
mDeviceStateCallback.onSupportedStatesChanged(newSupportedDeviceStates));
}
void notifyBaseStateChanged(int newBaseState) {
execute("notifyBaseStateChanged",
() -> mDeviceStateCallback.onBaseStateChanged(newBaseState));
}
void notifyStateChanged(int newDeviceState) {
execute("notifyStateChanged",
() -> mDeviceStateCallback.onStateChanged(newDeviceState));
}
void notifyDeviceStateChanged(DeviceState newDeviceState) {
execute("notifyDeviceStateChanged",
() -> mDeviceStateCallback.onDeviceStateChanged(newDeviceState));
}
private void execute(String traceName, Runnable r) {
mExecutor.execute(() -> {
if (DEBUG) {
Trace.beginSection(
mDeviceStateCallback.getClass().getSimpleName() + "#" + traceName);
}
try {
r.run();
} finally {
if (DEBUG) {
Trace.endSection();
}
}
});
}
}
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) {
validateRequestWrapperParameters(callback, executor);
mRequest = request;
mCallback = callback;
mExecutor = executor;
}
void notifyRequestActive() {
if (mCallback == null) {
return;
}
mExecutor.execute(() -> mCallback.onRequestActivated(mRequest));
}
void notifyRequestCanceled() {
if (mCallback == null) {
return;
}
mExecutor.execute(() -> mCallback.onRequestCanceled(mRequest));
}
private void validateRequestWrapperParameters(
@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.");
}
}
}
}