Add DeviceStateManagerService shell commands to get and override state.
This also introduces the CONTROL_DEVICE_STATE permission to protect
against setting the device state. The permission is granted to the shell
to allow overriding via ADB.
Two ADB commands are introduced with this change:
-> adb shell cmd device_state print-states
will print the list of states supported by the device
Ex:
$ adb shell cmd device_state print-states
[ 0, 1, 2 ]
-> adb shell cmd device_state state [reset|OVERRIDE_DEVICE_STATE]
will print the current device state or override the current device
state with the supplied override state.
Ex:
$ adb shell cmd device_state 0
Ex:
$ adb shell cmd device_state state
Device state: 0
----------------------
Base state: 2
Override state: 0
Bug: 159401801
Test: atest DeviceStateManagerServiceTest
Test: adb shell cmd device_state print-states
Test: adb shell cmd device_state state
Change-Id: I8a74f1f2bf8189523bdae861fb38f86988c21b2a
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f2af514..85eaa65 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5151,6 +5151,12 @@
<permission android:name="android.permission.INPUT_CONSUMER"
android:protectionLevel="signature" />
+ <!-- @hide Allows an application to control the system's device state managed by the
+ {@link android.service.devicestate.DeviceStateManagerService}. For example, on foldable
+ devices this would grant access to toggle between the folded and unfolded states. -->
+ <permission android:name="android.permission.CONTROL_DEVICE_STATE"
+ android:protectionLevel="signature" />
+
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 5f018a0..9cd69cc 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -338,6 +338,9 @@
<!-- Permissions required for CTS test - NotificationManagerTest -->
<uses-permission android:name="android.permission.MANAGE_NOTIFICATION_LISTENERS" />
+ <!-- Allows overriding the system's device state from the shell -->
+ <uses-permission android:name="android.permission.CONTROL_DEVICE_STATE"/>
+
<application android:label="@string/app_label"
android:theme="@android:style/Theme.DeviceDefault.DayNight"
android:defaultToDeviceProtectedStorage="true"
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index 7e3c1ab..1a28717 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -17,10 +17,14 @@
package com.android.server.devicestate;
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
+import static android.Manifest.permission.CONTROL_DEVICE_STATE;
import android.annotation.NonNull;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.hardware.devicestate.IDeviceStateManager;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
import android.util.IntArray;
import android.util.Slog;
@@ -29,6 +33,7 @@
import com.android.server.SystemService;
import com.android.server.policy.DeviceStatePolicyImpl;
+import java.io.FileDescriptor;
import java.util.Arrays;
/**
@@ -65,15 +70,20 @@
// The current committed device state.
@GuardedBy("mLock")
private int mCommittedState = INVALID_DEVICE_STATE;
- // The device state that is currently pending callback from the policy to be committed.
+ // The device state that is currently awaiting callback from the policy to be committed.
@GuardedBy("mLock")
private int mPendingState = INVALID_DEVICE_STATE;
// Whether or not the policy is currently waiting to be notified of the current pending state.
@GuardedBy("mLock")
private boolean mIsPolicyWaitingForState = false;
// The device state that is currently requested and is next to be configured and committed.
+ // Can be overwritten by an override state value if requested.
@GuardedBy("mLock")
private int mRequestedState = INVALID_DEVICE_STATE;
+ // The most recently requested override state, or INVALID_DEVICE_STATE if no override is
+ // requested.
+ @GuardedBy("mLock")
+ private int mRequestedOverrideState = INVALID_DEVICE_STATE;
public DeviceStateManagerService(@NonNull Context context) {
this(context, new DeviceStatePolicyImpl());
@@ -97,7 +107,6 @@
*
* @see #getPendingState()
*/
- @VisibleForTesting
int getCommittedState() {
synchronized (mLock) {
return mCommittedState;
@@ -119,13 +128,61 @@
* Returns the requested state. The service will configure the device to match the requested
* state when possible.
*/
- @VisibleForTesting
int getRequestedState() {
synchronized (mLock) {
return mRequestedState;
}
}
+ /**
+ * Overrides the current device state with the provided state.
+ *
+ * @return {@code true} if the override state is valid and supported, {@code false} otherwise.
+ */
+ boolean setOverrideState(int overrideState) {
+ if (getContext().checkCallingOrSelfPermission(CONTROL_DEVICE_STATE)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Must hold permission " + CONTROL_DEVICE_STATE);
+ }
+
+ synchronized (mLock) {
+ if (overrideState != INVALID_DEVICE_STATE && !isSupportedStateLocked(overrideState)) {
+ return false;
+ }
+
+ mRequestedOverrideState = overrideState;
+ updatePendingStateLocked();
+ }
+
+ notifyPolicyIfNeeded();
+ return true;
+ }
+
+ /**
+ * Clears an override state set with {@link #setOverrideState(int)}.
+ */
+ void clearOverrideState() {
+ setOverrideState(INVALID_DEVICE_STATE);
+ }
+
+ /**
+ * Returns the current requested override state, or {@link #INVALID_DEVICE_STATE} is no override
+ * state is requested.
+ */
+ int getOverrideState() {
+ synchronized (mLock) {
+ return mRequestedOverrideState;
+ }
+ }
+
+ /** Returns the list of currently supported device states. */
+ int[] getSupportedStates() {
+ synchronized (mLock) {
+ // Copy array to prevent external modification of internal state.
+ return Arrays.copyOf(mSupportedDeviceStates.toArray(), mSupportedDeviceStates.size());
+ }
+ }
+
private void updateSupportedStates(int[] supportedDeviceStates) {
// Must ensure sorted as isSupportedStateLocked() impl uses binary search.
Arrays.sort(supportedDeviceStates, 0, supportedDeviceStates.length);
@@ -135,11 +192,20 @@
if (mRequestedState != INVALID_DEVICE_STATE
&& !isSupportedStateLocked(mRequestedState)) {
// The current requested state is no longer valid. We'll clear it here, though
- // we won't actually update the current state with a call to
- // updatePendingStateLocked() as doing so will not have any effect.
+ // we won't actually update the current state until a callback comes from the
+ // provider with the most recent state.
mRequestedState = INVALID_DEVICE_STATE;
}
+ if (mRequestedOverrideState != INVALID_DEVICE_STATE
+ && !isSupportedStateLocked(mRequestedOverrideState)) {
+ // The current override state is no longer valid. We'll clear it here and update
+ // the committed state if necessary.
+ mRequestedOverrideState = INVALID_DEVICE_STATE;
+ }
+ updatePendingStateLocked();
}
+
+ notifyPolicyIfNeeded();
}
/**
@@ -168,27 +234,34 @@
}
/**
- * Tries to update the current configuring state with the current requested state. Must call
+ * Tries to update the current pending state with the current requested state. Must call
* {@link #notifyPolicyIfNeeded()} to actually notify the policy that the state is being
* changed.
*/
private void updatePendingStateLocked() {
- if (mRequestedState == INVALID_DEVICE_STATE) {
- // No currently requested state.
- return;
- }
-
if (mPendingState != INVALID_DEVICE_STATE) {
// Have pending state, can not configure a new state until the state is committed.
return;
}
- if (mRequestedState == mCommittedState) {
- // No need to notify the policy as the committed state matches the requested state.
+ int stateToConfigure;
+ if (mRequestedOverrideState != INVALID_DEVICE_STATE) {
+ stateToConfigure = mRequestedOverrideState;
+ } else {
+ stateToConfigure = mRequestedState;
+ }
+
+ if (stateToConfigure == INVALID_DEVICE_STATE) {
+ // No currently requested state.
return;
}
- mPendingState = mRequestedState;
+ if (stateToConfigure == mCommittedState) {
+ // The state requesting to be committed already matches the current committed state.
+ return;
+ }
+
+ mPendingState = stateToConfigure;
mIsPolicyWaitingForState = true;
}
@@ -271,6 +344,11 @@
/** Implementation of {@link IDeviceStateManager} published as a binder service. */
private final class BinderService extends IDeviceStateManager.Stub {
-
+ @Override // Binder call
+ public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
+ String[] args, ShellCallback callback, ResultReceiver result) {
+ new DeviceStateManagerShellCommand(DeviceStateManagerService.this)
+ .exec(this, in, out, err, args, callback, result);
+ }
}
}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
new file mode 100644
index 0000000..cf3b297
--- /dev/null
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
@@ -0,0 +1,128 @@
+/*
+ * 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 com.android.server.devicestate;
+
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
+
+import android.os.ShellCommand;
+
+import java.io.PrintWriter;
+
+/**
+ * ShellCommands for {@link DeviceStateManagerService}.
+ *
+ * Use with {@code adb shell cmd device_state ...}.
+ */
+public class DeviceStateManagerShellCommand extends ShellCommand {
+ private final DeviceStateManagerService mInternal;
+
+ public DeviceStateManagerShellCommand(DeviceStateManagerService service) {
+ mInternal = service;
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+ final PrintWriter pw = getOutPrintWriter();
+
+ switch (cmd) {
+ case "state":
+ return runState(pw);
+ case "print-states":
+ return runPrintStates(pw);
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ }
+
+ private void printState(PrintWriter pw) {
+ int committedState = mInternal.getCommittedState();
+ int requestedState = mInternal.getRequestedState();
+ int requestedOverrideState = mInternal.getOverrideState();
+
+ if (committedState == INVALID_DEVICE_STATE) {
+ pw.println("Device state: (invalid)");
+ } else {
+ pw.println("Device state: " + committedState);
+ }
+
+ if (requestedOverrideState != INVALID_DEVICE_STATE) {
+ pw.println("----------------------");
+ if (requestedState == INVALID_DEVICE_STATE) {
+ pw.println("Base state: (invalid)");
+ } else {
+ pw.println("Base state: " + requestedState);
+ }
+ pw.println("Override state: " + committedState);
+ }
+ }
+
+ private int runState(PrintWriter pw) {
+ final String nextArg = getNextArg();
+ if (nextArg == null) {
+ printState(pw);
+ } else if ("reset".equals(nextArg)) {
+ mInternal.clearOverrideState();
+ } else {
+ int requestedState;
+ try {
+ requestedState = Integer.parseInt(nextArg);
+ } catch (NumberFormatException e) {
+ getErrPrintWriter().println("Error: requested state should be an integer");
+ return -1;
+ }
+
+ boolean success = mInternal.setOverrideState(requestedState);
+ if (!success) {
+ getErrPrintWriter().println("Error: failed to set override state. Run:");
+ getErrPrintWriter().println("");
+ getErrPrintWriter().println(" print-states");
+ getErrPrintWriter().println("");
+ getErrPrintWriter().println("to get the list of currently supported device states");
+ return -1;
+ }
+ }
+ return 0;
+ }
+
+ private int runPrintStates(PrintWriter pw) {
+ int[] states = mInternal.getSupportedStates();
+ pw.print("Supported states: [ ");
+ for (int i = 0; i < states.length; i++) {
+ pw.print(states[i]);
+ if (i < states.length - 1) {
+ pw.print(", ");
+ }
+ }
+ pw.println(" ]");
+ return 0;
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("Device state manager (device_state) commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" state [reset|OVERRIDE_DEVICE_STATE]");
+ pw.println(" Return or override device state.");
+ pw.println(" print-states");
+ pw.println(" Return list of currently supported device states.");
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
index 058794a..9b182a7 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -105,6 +105,30 @@
}
@Test
+ public void requestOverrideState() {
+ mService.setOverrideState(OTHER_DEVICE_STATE);
+ // Committed state changes as there is a requested override.
+ assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE);
+ assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
+ assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), OTHER_DEVICE_STATE);
+
+ // Committed state is set back to the requested state once the override is cleared.
+ mService.clearOverrideState();
+ assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
+ assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
+ assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE);
+ }
+
+ @Test
+ public void requestOverrideState_unsupportedState() {
+ mService.setOverrideState(UNSUPPORTED_DEVICE_STATE);
+ // Committed state remains the same as the override state is unsupported.
+ assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
+ assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
+ assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE);
+ }
+
+ @Test
public void supportedStatesChanged() {
assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
assertEquals(mService.getPendingState(), INVALID_DEVICE_STATE);
@@ -146,6 +170,23 @@
assertEquals(mService.getRequestedState(), OTHER_DEVICE_STATE);
}
+ @Test
+ public void supportedStatesChanged_unsupportedOverrideState() {
+ mService.setOverrideState(OTHER_DEVICE_STATE);
+ // Committed state changes as there is a requested override.
+ assertEquals(mService.getCommittedState(), OTHER_DEVICE_STATE);
+ assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
+ assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), OTHER_DEVICE_STATE);
+
+ mProvider.notifySupportedDeviceStates(new int []{ DEFAULT_DEVICE_STATE });
+
+ // Committed state is set back to the requested state as the override state is no longer
+ // supported.
+ assertEquals(mService.getCommittedState(), DEFAULT_DEVICE_STATE);
+ assertEquals(mService.getRequestedState(), DEFAULT_DEVICE_STATE);
+ assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE);
+ }
+
private static final class TestDeviceStatePolicy implements DeviceStatePolicy {
private final DeviceStateProvider mProvider;
private int mLastDeviceStateRequestedToConfigure = INVALID_DEVICE_STATE;