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;
+        }
+    }
 }