Adds call to rear display overlay intent

When a request is made to enable rear display
mode, and it's from a process that doesn't
hold the CONTROL_DEVICE_STATE permission, we
should launch the educational overlay to alert
the user of what's about to happen.

This change also introduces a method on
DeviceStateManagerGlobal and Service to allow
the overlay activity to communicate back to the
system service if the overlay has been dismissed
or acted on

Bug: 207686851
Test: DeviceStateManagerServiceTest
 && DeviceStateManagerGlobalTest
Change-Id: Ife7d642563005a2571bdeacbd7a3baaa9aad5e25
This commit is contained in:
Kenneth Ford 2022-11-12 00:21:09 +00:00
parent dc078773aa
commit e65409d819
5 changed files with 181 additions and 4 deletions

View File

@ -52,6 +52,22 @@ public final class DeviceStateManager {
/** The maximum allowed device state identifier. */ /** The maximum allowed device state identifier. */
public static final int MAXIMUM_DEVICE_STATE = 255; 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; private final DeviceStateManagerGlobal mGlobal;
/** @hide */ /** @hide */

View File

@ -51,7 +51,7 @@ public final class DeviceStateManagerGlobal {
* connection with the device state service couldn't be established. * connection with the device state service couldn't be established.
*/ */
@Nullable @Nullable
static DeviceStateManagerGlobal getInstance() { public static DeviceStateManagerGlobal getInstance() {
synchronized (DeviceStateManagerGlobal.class) { synchronized (DeviceStateManagerGlobal.class) {
if (sInstance == null) { if (sInstance == null) {
IBinder b = ServiceManager.getService(Context.DEVICE_STATE_SERVICE); 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() { private void registerCallbackIfNeededLocked() {
if (mCallback == null) { if (mCallback == null) {
mCallback = new DeviceStateManagerCallback(); mCallback = new DeviceStateManagerCallback();

View File

@ -103,4 +103,15 @@ interface IDeviceStateManager {
@JavaPassthrough(annotation= @JavaPassthrough(annotation=
"@android.annotation.RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)") "@android.annotation.RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_STATE)")
void cancelBaseStateOverride(); 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);
} }

View File

@ -377,6 +377,11 @@ public final class DeviceStateManagerGlobalTest {
notifyDeviceStateInfoChanged(); 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) { public void setSupportedStates(int[] states) {
mSupportedStates = states; mSupportedStates = states;
notifyDeviceStateInfoChanged(); notifyDeviceStateInfoChanged();

View File

@ -17,6 +17,11 @@
package com.android.server.devicestate; package com.android.server.devicestate;
import static android.Manifest.permission.CONTROL_DEVICE_STATE; 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.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_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.IntRange;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.WindowConfiguration;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.hardware.devicestate.DeviceStateInfo; import android.hardware.devicestate.DeviceStateInfo;
import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManagerInternal; import android.hardware.devicestate.DeviceStateManagerInternal;
@ -157,6 +165,15 @@ public final class DeviceStateManagerService extends SystemService {
private Set<Integer> mDeviceStatesAvailableForAppRequests; private Set<Integer> mDeviceStatesAvailableForAppRequests;
private Set<Integer> mFoldedDeviceStates;
@Nullable
private DeviceState mRearDisplayState;
// TODO(259328837) Generalize for all pending feature requests in the future
@Nullable
private OverrideRequest mRearDisplayPendingOverrideRequest;
@VisibleForTesting @VisibleForTesting
interface SystemPropertySetter { interface SystemPropertySetter {
void setDebugTracingDeviceStateProperty(String value); void setDebugTracingDeviceStateProperty(String value);
@ -201,6 +218,7 @@ public final class DeviceStateManagerService extends SystemService {
synchronized (mLock) { synchronized (mLock) {
readStatesAvailableForRequestFromApps(); readStatesAvailableForRequestFromApps();
mFoldedDeviceStates = readFoldedStates();
} }
} }
@ -350,6 +368,8 @@ public final class DeviceStateManagerService extends SystemService {
mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers); mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers);
updatePendingStateLocked(); updatePendingStateLocked();
setRearDisplayStateLocked();
if (!mPendingState.isPresent()) { if (!mPendingState.isPresent()) {
// If the change in the supported states didn't result in a change of the pending // 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 // 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 * Returns {@code true} if the provided state is supported. Requires that
* {@link #mDeviceStates} is sorted prior to calling. * {@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. // Base state hasn't changed. Nothing to do.
return; 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); mBaseState = Optional.of(baseState);
if (baseState.hasFlag(FLAG_CANCEL_OVERRIDE_REQUESTS)) { 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, private void requestStateInternal(int state, int flags, int callingPid,
@NonNull IBinder token) { @NonNull IBinder token, boolean hasControlDeviceStatePermission) {
synchronized (mLock) { synchronized (mLock) {
final ProcessRecord processRecord = mProcessRecords.get(callingPid); final ProcessRecord processRecord = mProcessRecords.get(callingPid);
if (processRecord == null) { if (processRecord == null) {
@ -685,10 +718,34 @@ public final class DeviceStateManagerService extends SystemService {
OverrideRequest request = new OverrideRequest(token, callingPid, state, flags, OverrideRequest request = new OverrideRequest(token, callingPid, state, flags,
OVERRIDE_REQUEST_TYPE_EMULATED_STATE); 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) { private void cancelStateRequestInternal(int callingPid) {
synchronized (mLock) { synchronized (mLock) {
final ProcessRecord processRecord = mProcessRecords.get(callingPid); 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) { private void dumpInternal(PrintWriter pw) {
pw.println("DEVICE STATE MANAGER (dumpsys device_state)"); pw.println("DEVICE STATE MANAGER (dumpsys device_state)");
@ -823,6 +901,16 @@ public final class DeviceStateManagerService extends SystemService {
} }
} }
private Set<Integer> readFoldedStates() {
Set<Integer> 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") @GuardedBy("mLock")
private boolean isValidState(int state) { private boolean isValidState(int state) {
for (int i = 0; i < mDeviceStates.size(); i++) { for (int i = 0; i < mDeviceStates.size(); i++) {
@ -833,6 +921,28 @@ public final class DeviceStateManagerService extends SystemService {
return false; 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 { private final class DeviceStateProviderListener implements DeviceStateProvider.Listener {
@IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int mCurrentBaseState; @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) { if (identifier < MINIMUM_DEVICE_STATE || identifier > MAXIMUM_DEVICE_STATE) {
throw new IllegalArgumentException("Invalid identifier: " + identifier); throw new IllegalArgumentException("Invalid identifier: " + identifier);
} }
mCurrentBaseState = identifier; mCurrentBaseState = identifier;
setBaseState(identifier); setBaseState(identifier);
} }
@ -977,9 +1088,12 @@ public final class DeviceStateManagerService extends SystemService {
throw new IllegalArgumentException("Request token must not be null."); throw new IllegalArgumentException("Request token must not be null.");
} }
boolean hasControlStatePermission = getContext().checkCallingOrSelfPermission(
CONTROL_DEVICE_STATE) == PERMISSION_GRANTED;
final long callingIdentity = Binder.clearCallingIdentity(); final long callingIdentity = Binder.clearCallingIdentity();
try { try {
requestStateInternal(state, flags, callingPid, token); requestStateInternal(state, flags, callingPid, token, hasControlStatePermission);
} finally { } finally {
Binder.restoreCallingIdentity(callingIdentity); 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 @Override // Binder call
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver result) { String[] args, ShellCallback callback, ResultReceiver result) {