blob: bf07773f9038210f383c8879376226ef5bf5f71c [file] [log] [blame]
/*
* Copyright (C) 2022 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 androidx.window.extensions.area;
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import android.app.Activity;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateRequest;
import android.hardware.display.DisplayManager;
import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.util.Pair;
import android.view.Display;
import android.view.DisplayAddress;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.window.extensions.core.util.function.Consumer;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.ArrayUtils;
import java.util.concurrent.Executor;
/**
* Reference implementation of androidx.window.extensions.area OEM interface for use with
* WindowManager Jetpack.
*
* This component currently supports Rear Display mode with the ability to add and remove
* status listeners for this mode.
*
* The public methods in this class are thread-safe.
**/
public class WindowAreaComponentImpl implements WindowAreaComponent,
DeviceStateManager.DeviceStateCallback {
private final Object mLock = new Object();
@NonNull
private final DeviceStateManager mDeviceStateManager;
@NonNull
private final DisplayManager mDisplayManager;
@NonNull
private final Executor mExecutor;
@GuardedBy("mLock")
private final ArraySet<Consumer<Integer>> mRearDisplayStatusListeners = new ArraySet<>();
@GuardedBy("mLock")
private final ArraySet<Consumer<ExtensionWindowAreaStatus>>
mRearDisplayPresentationStatusListeners = new ArraySet<>();
private final int mRearDisplayState;
private final int mConcurrentDisplayState;
@NonNull
private final int[] mFoldedDeviceStates;
@NonNull
private long mRearDisplayAddress = 0;
@WindowAreaSessionState
private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
@GuardedBy("mLock")
private int mCurrentDeviceState = INVALID_DEVICE_STATE;
@GuardedBy("mLock")
private int[] mCurrentSupportedDeviceStates;
@GuardedBy("mLock")
private DeviceStateRequest mRearDisplayStateRequest;
@GuardedBy("mLock")
private RearDisplayPresentationController mRearDisplayPresentationController;
@Nullable
@GuardedBy("mLock")
private DisplayMetrics mRearDisplayMetrics;
@WindowAreaSessionState
@GuardedBy("mLock")
private int mLastReportedRearDisplayPresentationStatus;
public WindowAreaComponentImpl(@NonNull Context context) {
mDeviceStateManager = context.getSystemService(DeviceStateManager.class);
mDisplayManager = context.getSystemService(DisplayManager.class);
mExecutor = context.getMainExecutor();
mCurrentSupportedDeviceStates = mDeviceStateManager.getSupportedStates();
mFoldedDeviceStates = context.getResources().getIntArray(
R.array.config_foldedDeviceStates);
// TODO(b/236022708) Move rear display state to device state config file
mRearDisplayState = context.getResources().getInteger(
R.integer.config_deviceStateRearDisplay);
mConcurrentDisplayState = context.getResources().getInteger(
R.integer.config_deviceStateConcurrentRearDisplay);
mDeviceStateManager.registerCallback(mExecutor, this);
if (mConcurrentDisplayState != INVALID_DEVICE_STATE) {
mRearDisplayAddress = Long.parseLong(context.getResources().getString(
R.string.config_rearDisplayPhysicalAddress));
}
}
/**
* Adds a listener interested in receiving updates on the RearDisplayStatus
* of the device. Because this is being called from the OEM provided
* extensions, the result of the listener will be posted on the executor
* provided by the developer at the initial call site.
*
* Rear display mode moves the calling application to the display on the device that is
* facing the same direction as the rear cameras. This would be the cover display on a fold-in
* style device when the device is opened.
*
* Depending on the initial state of the device, the {@link Consumer} will receive either
* {@link WindowAreaComponent#STATUS_AVAILABLE} or
* {@link WindowAreaComponent#STATUS_UNAVAILABLE} if the feature is supported or not in that
* state respectively. When the rear display feature is triggered, the status is updated to be
* {@link WindowAreaComponent#STATUS_ACTIVE}.
* TODO(b/240727590): Prefix with AREA_
*
* @param consumer {@link Consumer} interested in receiving updates to the status of
* rear display mode.
*/
@Override
public void addRearDisplayStatusListener(
@NonNull Consumer<@WindowAreaStatus Integer> consumer) {
synchronized (mLock) {
mRearDisplayStatusListeners.add(consumer);
// If current device state is still invalid, the initial value has not been provided.
if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
return;
}
consumer.accept(getCurrentRearDisplayModeStatus());
}
}
/**
* Removes a listener no longer interested in receiving updates.
* @param consumer no longer interested in receiving updates to RearDisplayStatus
*/
@Override
public void removeRearDisplayStatusListener(
@NonNull Consumer<@WindowAreaStatus Integer> consumer) {
synchronized (mLock) {
mRearDisplayStatusListeners.remove(consumer);
}
}
/**
* Creates and starts a rear display session and provides updates to the
* callback provided. Because this is being called from the OEM provided
* extensions, the result of the listener will be posted on the executor
* provided by the developer at the initial call site.
*
* Rear display mode moves the calling application to the display on the device that is
* facing the same direction as the rear cameras. This would be the cover display on a fold-in
* style device when the device is opened.
*
* When rear display mode is enabled, a request is made to {@link DeviceStateManager}
* to override the device state to the state that corresponds to RearDisplay
* mode. When the {@link DeviceStateRequest} is activated, the provided {@link Consumer} is
* notified that the session is active by receiving
* {@link WindowAreaComponent#SESSION_STATE_ACTIVE}.
*
* @param activity to provide updates to the client on
* the status of the Session
* @param rearDisplaySessionCallback to provide updates to the client on
* the status of the Session
*/
@Override
public void startRearDisplaySession(@NonNull Activity activity,
@NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback) {
synchronized (mLock) {
if (mRearDisplayStateRequest != null) {
// Rear display session is already active
throw new IllegalStateException(
"Unable to start new rear display session as one is already active");
}
mRearDisplayStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build();
mDeviceStateManager.requestState(
mRearDisplayStateRequest,
mExecutor,
new RearDisplayStateRequestCallbackAdapter(rearDisplaySessionCallback)
);
}
}
/**
* Ends the current rear display session and provides updates to the
* callback provided. Because this is being called from the OEM provided
* extensions, the result of the listener will be posted on the executor
* provided by the developer at the initial call site.
*/
@Override
public void endRearDisplaySession() {
synchronized (mLock) {
if (mRearDisplayStateRequest != null || isRearDisplayActive()) {
mRearDisplayStateRequest = null;
mDeviceStateManager.cancelStateRequest();
} else {
throw new IllegalStateException(
"Unable to cancel a rear display session as there is no active session");
}
}
}
/**
* Adds a listener interested in receiving updates on the RearDisplayPresentationStatus
* of the device. Because this is being called from the OEM provided
* extensions, the result of the listener will be posted on the executor
* provided by the developer at the initial call site.
*
* Rear display presentation mode is a feature where an {@link Activity} can present
* additional content on a device with a second display that is facing the same direction
* as the rear camera (i.e. the cover display on a fold-in style device). The calling
* {@link Activity} does not move, whereas in rear display mode it does.
*
* This listener receives a {@link Pair} with the first item being the
* {@link WindowAreaComponent.WindowAreaStatus} that corresponds to the current status of the
* feature, and the second being the {@link DisplayMetrics} of the display that would be
* presented to when the feature is active.
*
* Depending on the initial state of the device, the {@link Consumer} will receive either
* {@link WindowAreaComponent#STATUS_AVAILABLE} or
* {@link WindowAreaComponent#STATUS_UNAVAILABLE} for the status value of the {@link Pair} if
* the feature is supported or not in that state respectively. Rear display presentation mode is
* currently not supported when the device is folded. When the rear display presentation feature
* is triggered, the status is updated to be {@link WindowAreaComponent#STATUS_UNAVAILABLE}.
* TODO(b/240727590): Prefix with AREA_
*
* TODO(b/239833099): Add a STATUS_ACTIVE option to let apps know if a feature is currently
* enabled.
*
* @param consumer {@link Consumer} interested in receiving updates to the status of
* rear display presentation mode.
*/
@Override
public void addRearDisplayPresentationStatusListener(
@NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
synchronized (mLock) {
mRearDisplayPresentationStatusListeners.add(consumer);
// If current device state is still invalid, the initial value has not been provided
if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
return;
}
@WindowAreaStatus int currentStatus = getCurrentRearDisplayPresentationModeStatus();
DisplayMetrics metrics =
currentStatus == STATUS_UNSUPPORTED ? null : getRearDisplayMetrics();
consumer.accept(
new RearDisplayPresentationStatus(currentStatus, metrics));
}
}
/**
* Removes a listener no longer interested in receiving updates.
* @param consumer no longer interested in receiving updates to RearDisplayPresentationStatus
*/
@Override
public void removeRearDisplayPresentationStatusListener(
@NonNull Consumer<ExtensionWindowAreaStatus> consumer) {
synchronized (mLock) {
mRearDisplayPresentationStatusListeners.remove(consumer);
}
}
/**
* Creates and starts a rear display presentation session and sends state updates to the
* consumer provided. This consumer will receive a constant represented by
* {@link WindowAreaSessionState} to represent the state of the current rear display
* session. It will be translated to a more friendly interface in the library.
*
* Because this is being called from the OEM provided extensions, the library
* will post the result of the listener on the executor provided by the developer.
*
* Rear display presentation mode refers to a feature where an {@link Activity} can present
* additional content on a device with a second display that is facing the same direction
* as the rear camera (i.e. the cover display on a fold-in style device). The calling
* {@link Activity} stays on the user-facing display.
*
* @param activity that the OEM implementation will use as a base
* context and to identify the source display area of the request.
* The reference to the activity instance must not be stored in the OEM
* implementation to prevent memory leaks.
* @param consumer to provide updates to the client on the status of the session
* @throws UnsupportedOperationException if this method is called when rear display presentation
* mode is not available. This could be to an incompatible device state or when
* another process is currently in this mode.
*/
@Override
public void startRearDisplayPresentationSession(@NonNull Activity activity,
@NonNull Consumer<@WindowAreaSessionState Integer> consumer) {
synchronized (mLock) {
if (mRearDisplayPresentationController != null) {
// Rear display presentation session is already active
throw new IllegalStateException(
"Unable to start new rear display presentation session as one is already "
+ "active");
}
if (getCurrentRearDisplayPresentationModeStatus()
!= WindowAreaComponent.STATUS_AVAILABLE) {
throw new IllegalStateException(
"Unable to start new rear display presentation session as the feature is "
+ "is not currently available");
}
mRearDisplayPresentationController = new RearDisplayPresentationController(activity,
stateStatus -> {
synchronized (mLock) {
if (stateStatus == SESSION_STATE_INACTIVE) {
// If the last reported session status was VISIBLE
// then the INVISIBLE state should be dispatched before INACTIVE
// due to not having a good mechanism to know when
// the content is no longer visible before it's fully removed
if (getLastReportedRearDisplayPresentationStatus()
== SESSION_STATE_VISIBLE) {
consumer.accept(SESSION_STATE_INVISIBLE);
}
mRearDisplayPresentationController = null;
}
mLastReportedRearDisplayPresentationStatus = stateStatus;
consumer.accept(stateStatus);
}
});
DeviceStateRequest concurrentDisplayStateRequest = DeviceStateRequest.newBuilder(
mConcurrentDisplayState).build();
mDeviceStateManager.requestState(
concurrentDisplayStateRequest,
mExecutor,
mRearDisplayPresentationController
);
}
}
/**
* Ends the current rear display presentation session and provides updates to the
* callback provided. When this is ended, the presented content from the calling
* {@link Activity} will also be removed from the rear facing display.
* Because this is being called from the OEM provided extensions, the result of the listener
* will be posted on the executor provided by the developer at the initial call site.
*
* Cancelling the {@link DeviceStateRequest} and exiting the rear display presentation state,
* will remove the presentation window from the cover display as the cover display is no longer
* enabled.
*/
@Override
public void endRearDisplayPresentationSession() {
synchronized (mLock) {
if (mRearDisplayPresentationController != null) {
mDeviceStateManager.cancelStateRequest();
} else {
throw new IllegalStateException(
"Unable to cancel a rear display presentation session as there is no "
+ "active session");
}
}
}
@Nullable
@Override
public ExtensionWindowAreaPresentation getRearDisplayPresentation() {
synchronized (mLock) {
ExtensionWindowAreaPresentation presentation = null;
if (mRearDisplayPresentationController != null) {
presentation = mRearDisplayPresentationController.getWindowAreaPresentation();
}
return presentation;
}
}
@Override
public void onSupportedStatesChanged(int[] supportedStates) {
synchronized (mLock) {
mCurrentSupportedDeviceStates = supportedStates;
updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
updateRearDisplayPresentationStatusListeners(
getCurrentRearDisplayPresentationModeStatus());
}
}
@Override
public void onStateChanged(int state) {
synchronized (mLock) {
mCurrentDeviceState = state;
updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
updateRearDisplayPresentationStatusListeners(
getCurrentRearDisplayPresentationModeStatus());
}
}
@GuardedBy("mLock")
private int getCurrentRearDisplayModeStatus() {
if (mRearDisplayState == INVALID_DEVICE_STATE) {
return WindowAreaComponent.STATUS_UNSUPPORTED;
}
if (!ArrayUtils.contains(mCurrentSupportedDeviceStates, mRearDisplayState)) {
return WindowAreaComponent.STATUS_UNAVAILABLE;
}
if (isRearDisplayActive()) {
return WindowAreaComponent.STATUS_ACTIVE;
}
return WindowAreaComponent.STATUS_AVAILABLE;
}
/**
* Helper method to determine if a rear display session is currently active by checking
* if the current device state is that which corresponds to {@code mRearDisplayState}.
*
* @return {@code true} if the device is in rear display state {@code false} if not
*/
@GuardedBy("mLock")
private boolean isRearDisplayActive() {
return mCurrentDeviceState == mRearDisplayState;
}
@GuardedBy("mLock")
private void updateRearDisplayStatusListeners(@WindowAreaStatus int windowAreaStatus) {
if (mRearDisplayState == INVALID_DEVICE_STATE) {
return;
}
synchronized (mLock) {
for (int i = 0; i < mRearDisplayStatusListeners.size(); i++) {
mRearDisplayStatusListeners.valueAt(i).accept(windowAreaStatus);
}
}
}
@GuardedBy("mLock")
private int getCurrentRearDisplayPresentationModeStatus() {
if (mConcurrentDisplayState == INVALID_DEVICE_STATE) {
return WindowAreaComponent.STATUS_UNSUPPORTED;
}
if (mCurrentDeviceState == mConcurrentDisplayState
|| !ArrayUtils.contains(mCurrentSupportedDeviceStates, mConcurrentDisplayState)
|| isDeviceFolded()) {
return WindowAreaComponent.STATUS_UNAVAILABLE;
}
return WindowAreaComponent.STATUS_AVAILABLE;
}
@GuardedBy("mLock")
private boolean isDeviceFolded() {
return ArrayUtils.contains(mFoldedDeviceStates, mCurrentDeviceState);
}
@GuardedBy("mLock")
private void updateRearDisplayPresentationStatusListeners(
@WindowAreaStatus int windowAreaStatus) {
if (mConcurrentDisplayState == INVALID_DEVICE_STATE) {
return;
}
RearDisplayPresentationStatus consumerValue = new RearDisplayPresentationStatus(
windowAreaStatus, getRearDisplayMetrics());
synchronized (mLock) {
for (int i = 0; i < mRearDisplayPresentationStatusListeners.size(); i++) {
mRearDisplayPresentationStatusListeners.valueAt(i).accept(consumerValue);
}
}
}
/**
* Returns the{@link DisplayMetrics} associated with the rear facing display. If the rear facing
* display was not found in the display list, but we have already computed the
* {@link DisplayMetrics} for that display, we return the cached value.
*
* TODO(b/267563768): Update with guidance from Display team for missing displays.
*
* @throws IllegalArgumentException if the display is not found and there is no cached
* {@link DisplayMetrics} for this display.
*/
@GuardedBy("mLock")
private DisplayMetrics getRearDisplayMetrics() {
Display[] displays = mDisplayManager.getDisplays(
DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
for (int i = 0; i < displays.length; i++) {
DisplayAddress.Physical address =
(DisplayAddress.Physical) displays[i].getAddress();
if (mRearDisplayAddress == address.getPhysicalDisplayId()) {
if (mRearDisplayMetrics == null) {
mRearDisplayMetrics = new DisplayMetrics();
}
displays[i].getRealMetrics(mRearDisplayMetrics);
return mRearDisplayMetrics;
}
}
if (mRearDisplayMetrics != null) {
return mRearDisplayMetrics;
} else {
throw new IllegalArgumentException(
"No display found with the provided display address");
}
}
@GuardedBy("mLock")
@WindowAreaSessionState
private int getLastReportedRearDisplayPresentationStatus() {
return mLastReportedRearDisplayPresentationStatus;
}
/**
* Callback for the {@link DeviceStateRequest} to be notified of when the request has been
* activated or cancelled. This callback provides information to the client library
* on the status of the RearDisplay session through {@code mRearDisplaySessionCallback}
*/
private class RearDisplayStateRequestCallbackAdapter implements DeviceStateRequest.Callback {
private final Consumer<Integer> mRearDisplaySessionCallback;
RearDisplayStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) {
mRearDisplaySessionCallback = callback;
}
@Override
public void onRequestActivated(@NonNull DeviceStateRequest request) {
synchronized (mLock) {
if (request.equals(mRearDisplayStateRequest)) {
mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE;
mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
}
}
}
@Override
public void onRequestCanceled(DeviceStateRequest request) {
synchronized (mLock) {
if (request.equals(mRearDisplayStateRequest)) {
mRearDisplayStateRequest = null;
}
mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
}
}
}
}