diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java index bdd45e6df448..dba1a5e8dfc6 100644 --- a/core/java/android/hardware/devicestate/DeviceStateManager.java +++ b/core/java/android/hardware/devicestate/DeviceStateManager.java @@ -52,6 +52,22 @@ public final class DeviceStateManager { /** The maximum allowed device state identifier. */ public static final int MAXIMUM_DEVICE_STATE = 255; + /** + * Intent needed to launch the rear display overlay activity from SysUI + * + * @hide + */ + public static final String ACTION_SHOW_REAR_DISPLAY_OVERLAY = + "com.android.intent.action.SHOW_REAR_DISPLAY_OVERLAY"; + + /** + * Intent extra sent to the rear display overlay activity of the current base state + * + * @hide + */ + public static final String EXTRA_ORIGINAL_DEVICE_BASE_STATE = + "original_device_base_state"; + private final DeviceStateManagerGlobal mGlobal; /** @hide */ diff --git a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java index 738045dafdf1..7756b9ca7e5a 100644 --- a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java +++ b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java @@ -51,7 +51,7 @@ public final class DeviceStateManagerGlobal { * connection with the device state service couldn't be established. */ @Nullable - static DeviceStateManagerGlobal getInstance() { + public static DeviceStateManagerGlobal getInstance() { synchronized (DeviceStateManagerGlobal.class) { if (sInstance == null) { IBinder b = ServiceManager.getService(Context.DEVICE_STATE_SERVICE); @@ -259,6 +259,22 @@ public final class DeviceStateManagerGlobal { } } + /** + * 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(); diff --git a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl index 7175eae58a26..099316099738 100644 --- a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl +++ b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl @@ -103,4 +103,15 @@ interface IDeviceStateManager { @JavaPassthrough(annotation= "@android.annotation.RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)") void cancelBaseStateOverride(); + + /** + * Notifies the system service that the educational overlay that was launched + * before entering a requested state was dismissed or closed, and provides + * the system information on if the pairing mode should be canceled or not. + * + * This should only be called from the overlay itself. + */ + @JavaPassthrough(annotation= + "@android.annotation.RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)") + void onStateRequestOverlayDismissed(boolean shouldCancelRequest); } diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java index 9e39e13265bd..3e3c77b7b21c 100644 --- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java +++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java @@ -377,6 +377,11 @@ public final class DeviceStateManagerGlobalTest { notifyDeviceStateInfoChanged(); } + // No-op in the test since DeviceStateManagerGlobal just calls into the system server with + // no business logic around it. + @Override + public void onStateRequestOverlayDismissed(boolean shouldCancelMode) {} + public void setSupportedStates(int[] states) { mSupportedStates = states; notifyDeviceStateInfoChanged(); diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index 44c8e18a22cf..925fc21737e5 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java @@ -17,6 +17,11 @@ package com.android.server.devicestate; import static android.Manifest.permission.CONTROL_DEVICE_STATE; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.hardware.devicestate.DeviceStateManager.ACTION_SHOW_REAR_DISPLAY_OVERLAY; +import static android.hardware.devicestate.DeviceStateManager.EXTRA_ORIGINAL_DEVICE_BASE_STATE; +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE; import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE; @@ -31,7 +36,10 @@ import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityOptions; +import android.app.WindowConfiguration; import android.content.Context; +import android.content.Intent; import android.hardware.devicestate.DeviceStateInfo; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManagerInternal; @@ -157,6 +165,15 @@ public final class DeviceStateManagerService extends SystemService { private Set mDeviceStatesAvailableForAppRequests; + private Set mFoldedDeviceStates; + + @Nullable + private DeviceState mRearDisplayState; + + // TODO(259328837) Generalize for all pending feature requests in the future + @Nullable + private OverrideRequest mRearDisplayPendingOverrideRequest; + @VisibleForTesting interface SystemPropertySetter { void setDebugTracingDeviceStateProperty(String value); @@ -201,6 +218,7 @@ public final class DeviceStateManagerService extends SystemService { synchronized (mLock) { readStatesAvailableForRequestFromApps(); + mFoldedDeviceStates = readFoldedStates(); } } @@ -350,6 +368,8 @@ public final class DeviceStateManagerService extends SystemService { mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers); updatePendingStateLocked(); + setRearDisplayStateLocked(); + 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 @@ -361,6 +381,15 @@ public final class DeviceStateManagerService extends SystemService { } } + @GuardedBy("mLock") + private void setRearDisplayStateLocked() { + int rearDisplayIdentifier = getContext().getResources().getInteger( + R.integer.config_deviceStateRearDisplay); + if (rearDisplayIdentifier != INVALID_DEVICE_STATE) { + mRearDisplayState = mDeviceStates.get(rearDisplayIdentifier); + } + } + /** * Returns {@code true} if the provided state is supported. Requires that * {@link #mDeviceStates} is sorted prior to calling. @@ -398,6 +427,10 @@ public final class DeviceStateManagerService extends SystemService { // Base state hasn't changed. Nothing to do. return; } + // There is a pending rear display request, so we check if the overlay should be closed + if (mRearDisplayPendingOverrideRequest != null) { + handleRearDisplayBaseStateChangedLocked(identifier); + } mBaseState = Optional.of(baseState); if (baseState.hasFlag(FLAG_CANCEL_OVERRIDE_REQUESTS)) { @@ -663,7 +696,7 @@ public final class DeviceStateManagerService extends SystemService { } private void requestStateInternal(int state, int flags, int callingPid, - @NonNull IBinder token) { + @NonNull IBinder token, boolean hasControlDeviceStatePermission) { synchronized (mLock) { final ProcessRecord processRecord = mProcessRecords.get(callingPid); if (processRecord == null) { @@ -685,10 +718,34 @@ public final class DeviceStateManagerService extends SystemService { OverrideRequest request = new OverrideRequest(token, callingPid, state, flags, OVERRIDE_REQUEST_TYPE_EMULATED_STATE); - mOverrideRequestController.addRequest(request); + + // If we don't have the CONTROL_DEVICE_STATE permission, we want to show the overlay + if (!hasControlDeviceStatePermission && mRearDisplayState != null + && state == mRearDisplayState.getIdentifier()) { + showRearDisplayEducationalOverlayLocked(request); + } else { + mOverrideRequestController.addRequest(request); + } } } + /** + * If we get a request to enter rear display mode, we need to display an educational + * overlay to let the user know what will happen. This creates the pending request and then + * launches the {@link RearDisplayEducationActivity} + */ + @GuardedBy("mLock") + private void showRearDisplayEducationalOverlayLocked(OverrideRequest request) { + mRearDisplayPendingOverrideRequest = request; + + Intent intent = new Intent(ACTION_SHOW_REAR_DISPLAY_OVERLAY); + intent.setFlags(FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(EXTRA_ORIGINAL_DEVICE_BASE_STATE, mBaseState.get().getIdentifier()); + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); + getUiContext().startActivity(intent, options.toBundle()); + } + private void cancelStateRequestInternal(int callingPid) { synchronized (mLock) { final ProcessRecord processRecord = mProcessRecords.get(callingPid); @@ -738,6 +795,27 @@ public final class DeviceStateManagerService extends SystemService { } } + /** + * Adds the rear display state request to the {@link OverrideRequestController} if the + * educational overlay was closed in a way that should enable the feature, and cancels the + * request if it was dismissed in a way that should cancel the feature. + */ + private void onStateRequestOverlayDismissedInternal(boolean shouldCancelRequest) { + if (mRearDisplayPendingOverrideRequest != null) { + synchronized (mLock) { + if (shouldCancelRequest) { + ProcessRecord processRecord = mProcessRecords.get( + mRearDisplayPendingOverrideRequest.getPid()); + processRecord.notifyRequestCanceledAsync( + mRearDisplayPendingOverrideRequest.getToken()); + } else { + mOverrideRequestController.addRequest(mRearDisplayPendingOverrideRequest); + } + mRearDisplayPendingOverrideRequest = null; + } + } + } + private void dumpInternal(PrintWriter pw) { pw.println("DEVICE STATE MANAGER (dumpsys device_state)"); @@ -823,6 +901,16 @@ public final class DeviceStateManagerService extends SystemService { } } + private Set readFoldedStates() { + Set foldedStates = new HashSet(); + int[] mFoldedStatesArray = getContext().getResources().getIntArray( + com.android.internal.R.array.config_foldedDeviceStates); + for (int i = 0; i < mFoldedStatesArray.length; i++) { + foldedStates.add(mFoldedStatesArray[i]); + } + return foldedStates; + } + @GuardedBy("mLock") private boolean isValidState(int state) { for (int i = 0; i < mDeviceStates.size(); i++) { @@ -833,6 +921,28 @@ public final class DeviceStateManagerService extends SystemService { return false; } + /** + * If the device is being opened, in response to the rear display educational overlay, we should + * dismiss the overlay and enter the mode. + */ + @GuardedBy("mLock") + private void handleRearDisplayBaseStateChangedLocked(int newBaseState) { + if (isDeviceOpeningLocked(newBaseState)) { + onStateRequestOverlayDismissedInternal(false); + } + } + + /** + * Determines if the device is being opened and if we are going from a folded state to a + * non-folded state. + */ + @GuardedBy("mLock") + private boolean isDeviceOpeningLocked(int newBaseState) { + return mBaseState.filter( + deviceState -> mFoldedDeviceStates.contains(deviceState.getIdentifier()) + && !mFoldedDeviceStates.contains(newBaseState)).isPresent(); + } + private final class DeviceStateProviderListener implements DeviceStateProvider.Listener { @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int mCurrentBaseState; @@ -850,6 +960,7 @@ public final class DeviceStateManagerService extends SystemService { if (identifier < MINIMUM_DEVICE_STATE || identifier > MAXIMUM_DEVICE_STATE) { throw new IllegalArgumentException("Invalid identifier: " + identifier); } + mCurrentBaseState = identifier; setBaseState(identifier); } @@ -977,9 +1088,12 @@ public final class DeviceStateManagerService extends SystemService { throw new IllegalArgumentException("Request token must not be null."); } + boolean hasControlStatePermission = getContext().checkCallingOrSelfPermission( + CONTROL_DEVICE_STATE) == PERMISSION_GRANTED; + final long callingIdentity = Binder.clearCallingIdentity(); try { - requestStateInternal(state, flags, callingPid, token); + requestStateInternal(state, flags, callingPid, token, hasControlStatePermission); } finally { Binder.restoreCallingIdentity(callingIdentity); } @@ -1033,6 +1147,21 @@ public final class DeviceStateManagerService extends SystemService { } } + @Override // Binder call + public void onStateRequestOverlayDismissed(boolean shouldCancelRequest) { + + getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE, + "CONTROL_DEVICE_STATE permission required to control the state request " + + "overlay"); + + final long callingIdentity = Binder.clearCallingIdentity(); + try { + onStateRequestOverlayDismissedInternal(shouldCancelRequest); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + @Override // Binder call public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver result) {