Add onDeviceStateChanged() callback to DeviceStateManager.
This change introduces the DeviceStateCallback API to DeviceStateManager
which allows processes outside system_server (as well as system_server)
to receive notifications about changes in the system's device state.
Test: atest DeviceStateManagerServiceTest
Bug: 159401801
Change-Id: I9ccdbd8c4a51858a13b0152ed39a8bf20e41e64f
diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java
index a52f983..29a6ee2 100644
--- a/core/java/android/hardware/devicestate/DeviceStateManager.java
+++ b/core/java/android/hardware/devicestate/DeviceStateManager.java
@@ -16,9 +16,12 @@
package android.hardware.devicestate;
+import android.annotation.NonNull;
import android.annotation.SystemService;
import android.content.Context;
+import java.util.concurrent.Executor;
+
/**
* Manages the state of the system for devices with user-configurable hardware like a foldable
* phone.
@@ -33,6 +36,47 @@
private DeviceStateManagerGlobal mGlobal;
public DeviceStateManager() {
- mGlobal = DeviceStateManagerGlobal.getInstance();
+ DeviceStateManagerGlobal global = DeviceStateManagerGlobal.getInstance();
+ if (global == null) {
+ throw new IllegalStateException(
+ "Failed to get instance of global device state manager.");
+ }
+ mGlobal = global;
+ }
+
+ /**
+ * Registers a listener to receive notifications about changes in device state.
+ *
+ * @param listener the listener to register.
+ * @param executor the executor to process notifications.
+ *
+ * @see DeviceStateListener
+ */
+ public void registerDeviceStateListener(@NonNull DeviceStateListener listener,
+ @NonNull Executor executor) {
+ mGlobal.registerDeviceStateListener(listener, executor);
+ }
+
+ /**
+ * Unregisters a listener previously registered with
+ * {@link #registerDeviceStateListener(DeviceStateListener, Executor)}.
+ */
+ public void unregisterDeviceStateListener(@NonNull DeviceStateListener listener) {
+ mGlobal.unregisterDeviceStateListener(listener);
+ }
+
+ /**
+ * Listens for changes in device states.
+ */
+ public interface DeviceStateListener {
+ /**
+ * Called in response to device state changes.
+ * <p>
+ * Guaranteed to be called once on registration of the listener with the
+ * initial value and then on every subsequent change in device state.
+ *
+ * @param deviceState the new device state.
+ */
+ void onDeviceStateChanged(int deviceState);
}
}
diff --git a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java
index 4e7cf4a..c8905038 100644
--- a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java
+++ b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java
@@ -19,16 +19,26 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.hardware.devicestate.DeviceStateManager.DeviceStateListener;
import android.os.IBinder;
+import android.os.RemoteException;
import android.os.ServiceManager;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+
/**
* Provides communication with the device state system service on behalf of applications.
*
* @see DeviceStateManager
* @hide
*/
-final class DeviceStateManagerGlobal {
+@VisibleForTesting(visibility = Visibility.PACKAGE)
+public final class DeviceStateManagerGlobal {
private static DeviceStateManagerGlobal sInstance;
/**
@@ -49,10 +59,121 @@
}
}
+ private final Object mLock = new Object();
@NonNull
private final IDeviceStateManager mDeviceStateManager;
+ @Nullable
+ private DeviceStateManagerCallback mCallback;
- private DeviceStateManagerGlobal(@NonNull IDeviceStateManager deviceStateManager) {
+ @GuardedBy("mLock")
+ private final ArrayList<DeviceStateListenerWrapper> mListeners = new ArrayList<>();
+ @Nullable
+ @GuardedBy("mLock")
+ private Integer mLastReceivedState;
+
+ @VisibleForTesting
+ public DeviceStateManagerGlobal(@NonNull IDeviceStateManager deviceStateManager) {
mDeviceStateManager = deviceStateManager;
}
+
+ /**
+ * Registers a listener to receive notifications about changes in device state.
+ *
+ * @see DeviceStateManager#registerDeviceStateListener(DeviceStateListener, Executor)
+ */
+ @VisibleForTesting(visibility = Visibility.PACKAGE)
+ public void registerDeviceStateListener(@NonNull DeviceStateListener listener,
+ @NonNull Executor executor) {
+ Integer stateToReport;
+ DeviceStateListenerWrapper wrapper;
+ synchronized (mLock) {
+ registerCallbackIfNeededLocked();
+
+ int index = findListenerLocked(listener);
+ if (index != -1) {
+ // This listener is already registered.
+ return;
+ }
+
+ wrapper = new DeviceStateListenerWrapper(listener, executor);
+ mListeners.add(wrapper);
+ stateToReport = mLastReceivedState;
+ }
+
+ if (stateToReport != null) {
+ // Notify the listener with the most recent device state from the server. If the state
+ // to report is null this is likely the first listener added and we're still waiting
+ // from the callback from the server.
+ wrapper.notifyDeviceStateChanged(stateToReport);
+ }
+ }
+
+ /**
+ * Unregisters a listener previously registered with
+ * {@link #registerDeviceStateListener(DeviceStateListener, Executor)}.
+ *
+ * @see DeviceStateManager#registerDeviceStateListener(DeviceStateListener, Executor)
+ */
+ @VisibleForTesting(visibility = Visibility.PACKAGE)
+ public void unregisterDeviceStateListener(DeviceStateListener listener) {
+ synchronized (mLock) {
+ int indexToRemove = findListenerLocked(listener);
+ if (indexToRemove != -1) {
+ mListeners.remove(indexToRemove);
+ }
+ }
+ }
+
+ private void registerCallbackIfNeededLocked() {
+ if (mCallback == null) {
+ mCallback = new DeviceStateManagerCallback();
+ try {
+ mDeviceStateManager.registerCallback(mCallback);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ private int findListenerLocked(DeviceStateListener listener) {
+ for (int i = 0; i < mListeners.size(); i++) {
+ if (mListeners.get(i).mDeviceStateListener.equals(listener)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void handleDeviceStateChanged(int newDeviceState) {
+ ArrayList<DeviceStateListenerWrapper> listeners;
+ synchronized (mLock) {
+ mLastReceivedState = newDeviceState;
+ listeners = new ArrayList<>(mListeners);
+ }
+
+ for (int i = 0; i < listeners.size(); i++) {
+ listeners.get(i).notifyDeviceStateChanged(newDeviceState);
+ }
+ }
+
+ private final class DeviceStateManagerCallback extends IDeviceStateManagerCallback.Stub {
+ @Override
+ public void onDeviceStateChanged(int deviceState) {
+ handleDeviceStateChanged(deviceState);
+ }
+ }
+
+ private static final class DeviceStateListenerWrapper {
+ private final DeviceStateListener mDeviceStateListener;
+ private final Executor mExecutor;
+
+ DeviceStateListenerWrapper(DeviceStateListener listener, Executor executor) {
+ mDeviceStateListener = listener;
+ mExecutor = executor;
+ }
+
+ void notifyDeviceStateChanged(int newDeviceState) {
+ mExecutor.execute(() -> mDeviceStateListener.onDeviceStateChanged(newDeviceState));
+ }
+ }
}
diff --git a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl
index 24913e9..a157b33 100644
--- a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl
+++ b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl
@@ -16,5 +16,9 @@
package android.hardware.devicestate;
+import android.hardware.devicestate.IDeviceStateManagerCallback;
+
/** @hide */
-interface IDeviceStateManager {}
+interface IDeviceStateManager {
+ void registerCallback(in IDeviceStateManagerCallback callback);
+}
diff --git a/core/java/android/hardware/devicestate/IDeviceStateManagerCallback.aidl b/core/java/android/hardware/devicestate/IDeviceStateManagerCallback.aidl
new file mode 100644
index 0000000..d1c5813
--- /dev/null
+++ b/core/java/android/hardware/devicestate/IDeviceStateManagerCallback.aidl
@@ -0,0 +1,22 @@
+/**
+ * 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;
+
+/** @hide */
+interface IDeviceStateManagerCallback {
+ oneway void onDeviceStateChanged(int deviceState);
+}
diff --git a/core/tests/devicestatetests/Android.bp b/core/tests/devicestatetests/Android.bp
new file mode 100644
index 0000000..409b77b
--- /dev/null
+++ b/core/tests/devicestatetests/Android.bp
@@ -0,0 +1,26 @@
+// 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.
+
+android_test {
+ name: "FrameworksCoreDeviceStateManagerTests",
+ // Include all test java files
+ srcs: ["src/**/*.java"],
+ static_libs: [
+ "androidx.test.rules",
+ "frameworks-base-testutils",
+ ],
+ libs: ["android.test.runner"],
+ platform_apis: true,
+ certificate: "platform",
+}
diff --git a/core/tests/devicestatetests/AndroidManifest.xml b/core/tests/devicestatetests/AndroidManifest.xml
new file mode 100644
index 0000000..acd6e7b
--- /dev/null
+++ b/core/tests/devicestatetests/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.frameworks.coretests.devicestate"
+ android:sharedUserId="android.uid.system" >
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.frameworks.coretests.devicestate"
+ android:label="Device State Manager Tests"/>
+
+</manifest>
diff --git a/core/tests/devicestatetests/AndroidTest.xml b/core/tests/devicestatetests/AndroidTest.xml
new file mode 100644
index 0000000..9e38afd
--- /dev/null
+++ b/core/tests/devicestatetests/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Runs Device State Manager Tests.">
+ <option name="test-suite-tag" value="apct"/>
+ <option name="test-suite-tag" value="apct-instrumentation"/>
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="FrameworksCoreDeviceStateManagerTests.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.frameworks.coretests.devicestate" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false" />
+ </test>
+</configuration>
diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
new file mode 100644
index 0000000..36f01f9
--- /dev/null
+++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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 static junit.framework.Assert.assertEquals;
+
+import android.annotation.Nullable;
+import android.os.RemoteException;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.ConcurrentUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link DeviceStateManagerGlobal}.
+ * <p/>
+ * Run with <code>atest DeviceStateManagerGlobalTest</code>.
+ */
+@RunWith(JUnit4.class)
+@SmallTest
+public final class DeviceStateManagerGlobalTest {
+ private TestDeviceStateManagerService mService;
+ private DeviceStateManagerGlobal mDeviceStateManagerGlobal;
+
+ @Before
+ public void setUp() {
+ mService = new TestDeviceStateManagerService();
+ mDeviceStateManagerGlobal = new DeviceStateManagerGlobal(mService);
+ }
+
+ @Test
+ public void registerListener() {
+ mService.setDeviceState(0);
+
+ TestDeviceStateListener listener1 = new TestDeviceStateListener();
+ TestDeviceStateListener listener2 = new TestDeviceStateListener();
+
+ mDeviceStateManagerGlobal.registerDeviceStateListener(listener1,
+ ConcurrentUtils.DIRECT_EXECUTOR);
+ mDeviceStateManagerGlobal.registerDeviceStateListener(listener2,
+ ConcurrentUtils.DIRECT_EXECUTOR);
+ assertEquals(0, listener1.getLastReportedState().intValue());
+ assertEquals(0, listener2.getLastReportedState().intValue());
+
+ mService.setDeviceState(1);
+ assertEquals(1, listener1.getLastReportedState().intValue());
+ assertEquals(1, listener2.getLastReportedState().intValue());
+ }
+
+ @Test
+ public void unregisterListener() {
+ mService.setDeviceState(0);
+
+ TestDeviceStateListener listener = new TestDeviceStateListener();
+
+ mDeviceStateManagerGlobal.registerDeviceStateListener(listener,
+ ConcurrentUtils.DIRECT_EXECUTOR);
+ assertEquals(0, listener.getLastReportedState().intValue());
+
+ mDeviceStateManagerGlobal.unregisterDeviceStateListener(listener);
+
+ mService.setDeviceState(1);
+ assertEquals(0, listener.getLastReportedState().intValue());
+ }
+
+ private final class TestDeviceStateListener implements DeviceStateManager.DeviceStateListener {
+ @Nullable
+ private Integer mLastReportedDeviceState;
+
+ @Override
+ public void onDeviceStateChanged(int deviceState) {
+ mLastReportedDeviceState = deviceState;
+ }
+
+ @Nullable
+ public Integer getLastReportedState() {
+ return mLastReportedDeviceState;
+ }
+ }
+
+ private final class TestDeviceStateManagerService extends IDeviceStateManager.Stub {
+ private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
+ private Set<IDeviceStateManagerCallback> mCallbacks = new HashSet<>();
+
+ @Override
+ public void registerCallback(IDeviceStateManagerCallback callback) {
+ if (mCallbacks.contains(callback)) {
+ throw new SecurityException("Callback is already registered.");
+ }
+
+ mCallbacks.add(callback);
+ try {
+ callback.onDeviceStateChanged(mDeviceState);
+ } catch (RemoteException e) {
+ // Do nothing. Should never happen.
+ }
+ }
+
+ public void setDeviceState(int deviceState) {
+ boolean stateChanged = mDeviceState != deviceState;
+ mDeviceState = deviceState;
+ if (stateChanged) {
+ for (IDeviceStateManagerCallback callback : mCallbacks) {
+ try {
+ callback.onDeviceStateChanged(mDeviceState);
+ } catch (RemoteException e) {
+ // Do nothing. Should never happen.
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index c51c38d..3172a04 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -23,11 +23,15 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.devicestate.IDeviceStateManager;
+import android.hardware.devicestate.IDeviceStateManagerCallback;
import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.util.IntArray;
import android.util.Slog;
+import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -37,6 +41,7 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Arrays;
/**
@@ -66,6 +71,8 @@
private final Object mLock = new Object();
@NonNull
private final DeviceStatePolicy mDeviceStatePolicy;
+ @NonNull
+ private final BinderService mBinderService;
@GuardedBy("mLock")
private IntArray mSupportedDeviceStates;
@@ -88,6 +95,10 @@
@GuardedBy("mLock")
private int mRequestedOverrideState = INVALID_DEVICE_STATE;
+ // List of registered callbacks indexed by process id.
+ @GuardedBy("mLock")
+ private final SparseArray<CallbackRecord> mCallbacks = new SparseArray<>();
+
public DeviceStateManagerService(@NonNull Context context) {
this(context, new DeviceStatePolicyImpl());
}
@@ -97,11 +108,12 @@
super(context);
mDeviceStatePolicy = policy;
mDeviceStatePolicy.getDeviceStateProvider().setListener(new DeviceStateProviderListener());
+ mBinderService = new BinderService();
}
@Override
public void onStart() {
- publishBinderService(Context.DEVICE_STATE_SERVICE, new BinderService());
+ publishBinderService(Context.DEVICE_STATE_SERVICE, mBinderService);
}
/**
@@ -186,6 +198,11 @@
}
}
+ @VisibleForTesting
+ IDeviceStateManager getBinderService() {
+ return mBinderService;
+ }
+
private void updateSupportedStates(int[] supportedDeviceStates) {
// Must ensure sorted as isSupportedStateLocked() impl uses binary search.
Arrays.sort(supportedDeviceStates, 0, supportedDeviceStates.length);
@@ -310,18 +327,81 @@
* </p>
*/
private void commitPendingState() {
+ // Update the current state.
+ int newState;
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "Committing state: " + mPendingState);
}
mCommittedState = mPendingState;
+ newState = mCommittedState;
mPendingState = INVALID_DEVICE_STATE;
updatePendingStateLocked();
}
+ // Notify callbacks of a change.
+ notifyDeviceStateChanged(newState);
+
+ // Try to configure the next state if needed.
notifyPolicyIfNeeded();
}
+ private void notifyDeviceStateChanged(int deviceState) {
+ if (Thread.holdsLock(mLock)) {
+ throw new IllegalStateException(
+ "Attempting to notify callbacks with service lock held.");
+ }
+
+ // Grab the lock and copy the callbacks.
+ ArrayList<CallbackRecord> callbacks;
+ synchronized (mLock) {
+ if (mCallbacks.size() == 0) {
+ return;
+ }
+
+ callbacks = new ArrayList<>();
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ callbacks.add(mCallbacks.valueAt(i));
+ }
+ }
+
+ // After releasing the lock, send the notifications out.
+ for (int i = 0; i < callbacks.size(); i++) {
+ callbacks.get(i).notifyDeviceStateAsync(deviceState);
+ }
+ }
+
+ private void registerCallbackInternal(IDeviceStateManagerCallback callback, int callingPid) {
+ int currentState;
+ CallbackRecord record;
+ // Grab the lock to register the callback and get the current state.
+ synchronized (mLock) {
+ if (mCallbacks.contains(callingPid)) {
+ throw new SecurityException("The calling process has already registered an"
+ + " IDeviceStateManagerCallback.");
+ }
+
+ record = new CallbackRecord(callback, callingPid);
+ try {
+ callback.asBinder().linkToDeath(record, 0);
+ } catch (RemoteException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ mCallbacks.put(callingPid, record);
+ currentState = mCommittedState;
+ }
+
+ // Notify the callback of the state at registration.
+ record.notifyDeviceStateAsync(currentState);
+ }
+
+ private void unregisterCallbackInternal(CallbackRecord record) {
+ synchronized (mLock) {
+ mCallbacks.remove(record.mPid);
+ }
+ }
+
private void dumpInternal(PrintWriter pw) {
pw.println("DEVICE STATE MANAGER (dumpsys device_state)");
@@ -330,6 +410,14 @@
pw.println(" mPendingState=" + toString(mPendingState));
pw.println(" mRequestedState=" + toString(mRequestedState));
pw.println(" mRequestedOverrideState=" + toString(mRequestedOverrideState));
+
+ final int callbackCount = mCallbacks.size();
+ pw.println();
+ pw.println("Callbacks: size=" + callbackCount);
+ for (int i = 0; i < callbackCount; i++) {
+ CallbackRecord callback = mCallbacks.valueAt(i);
+ pw.println(" " + i + ": mPid=" + callback.mPid);
+ }
}
}
@@ -360,9 +448,48 @@
}
}
+ private final class CallbackRecord implements IBinder.DeathRecipient {
+ private final IDeviceStateManagerCallback mCallback;
+ private final int mPid;
+
+ CallbackRecord(IDeviceStateManagerCallback callback, int pid) {
+ mCallback = callback;
+ mPid = pid;
+ }
+
+ @Override
+ public void binderDied() {
+ unregisterCallbackInternal(this);
+ }
+
+ public void notifyDeviceStateAsync(int devicestate) {
+ try {
+ mCallback.onDeviceStateChanged(devicestate);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify process " + mPid + " that device state changed.",
+ ex);
+ }
+ }
+ }
+
/** Implementation of {@link IDeviceStateManager} published as a binder service. */
private final class BinderService extends IDeviceStateManager.Stub {
@Override // Binder call
+ public void registerCallback(IDeviceStateManagerCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("Device state callback must not be null.");
+ }
+
+ final int callingPid = Binder.getCallingPid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ registerCallbackInternal(callback, callingPid);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override // Binder call
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver result) {
new DeviceStateManagerShellCommand(DeviceStateManagerService.this)
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 0e58be3..95aac60 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -19,8 +19,11 @@
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertThrows;
+import android.hardware.devicestate.IDeviceStateManagerCallback;
+import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import androidx.test.InstrumentationRegistry;
@@ -30,6 +33,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import javax.annotation.Nullable;
+
/**
* Unit tests for {@link DeviceStateManagerService}.
* <p/>
@@ -189,6 +194,38 @@
assertEquals(mPolicy.getMostRecentRequestedStateToConfigure(), DEFAULT_DEVICE_STATE);
}
+ @Test
+ public void registerCallback() throws RemoteException {
+ TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
+ mService.getBinderService().registerCallback(callback);
+
+ mProvider.notifyRequestState(OTHER_DEVICE_STATE);
+ assertNotNull(callback.getLastNotifiedValue());
+ assertEquals(callback.getLastNotifiedValue().intValue(), OTHER_DEVICE_STATE);
+
+ mProvider.notifyRequestState(DEFAULT_DEVICE_STATE);
+ assertEquals(callback.getLastNotifiedValue().intValue(), DEFAULT_DEVICE_STATE);
+
+ mPolicy.blockConfigure();
+ mProvider.notifyRequestState(OTHER_DEVICE_STATE);
+ // The callback should not have been notified of the state change as the policy is still
+ // pending callback.
+ assertEquals(callback.getLastNotifiedValue().intValue(), DEFAULT_DEVICE_STATE);
+
+ mPolicy.resumeConfigure();
+ // Now that the policy is finished processing the callback should be notified of the state
+ // change.
+ assertEquals(callback.getLastNotifiedValue().intValue(), OTHER_DEVICE_STATE);
+ }
+
+ @Test
+ public void registerCallback_emitsInitialValue() throws RemoteException {
+ TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
+ mService.getBinderService().registerCallback(callback);
+ assertNotNull(callback.getLastNotifiedValue());
+ assertEquals(callback.getLastNotifiedValue().intValue(), DEFAULT_DEVICE_STATE);
+ }
+
private static final class TestDeviceStatePolicy implements DeviceStatePolicy {
private final DeviceStateProvider mProvider;
private int mLastDeviceStateRequestedToConfigure = INVALID_DEVICE_STATE;
@@ -264,4 +301,19 @@
mListener.onStateChanged(state);
}
}
+
+ private static final class TestDeviceStateManagerCallback extends
+ IDeviceStateManagerCallback.Stub {
+ Integer mLastNotifiedValue;
+
+ @Override
+ public void onDeviceStateChanged(int deviceState) {
+ mLastNotifiedValue = deviceState;
+ }
+
+ @Nullable
+ Integer getLastNotifiedValue() {
+ return mLastNotifiedValue;
+ }
+ }
}