ADPF: Add HintManagerService

Test: Manual test, run bouncy ball
Test: atest HintManagerServiceTest
Test: adb shell dumpsys hint
Bug: 158791282
Change-Id: I50b19ab7629f006decbcddd653fb67588fc4160b
Signed-off-by: Wei Wang <wvw@google.com>
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
new file mode 100644
index 0000000..fc7628c
--- /dev/null
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2021 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.power.hint;
+
+import android.app.ActivityManager;
+import android.app.IUidObserver;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IHintManager;
+import android.os.IHintSession;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.Preconditions;
+import com.android.server.FgThread;
+import com.android.server.SystemService;
+import com.android.server.utils.Slogf;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+/** An hint service implementation that runs in System Server process. */
+public final class HintManagerService extends SystemService {
+    private static final String TAG = "HintManagerService";
+    private static final boolean DEBUG = false;
+    @VisibleForTesting final long mHintSessionPreferredRate;
+
+    @GuardedBy("mLock")
+    private final ArrayMap<Integer, ArrayMap<IBinder, AppHintSession>> mActiveSessions;
+
+    /** Lock to protect HAL handles and listen list. */
+    private final Object mLock = new Object();
+
+    @VisibleForTesting final UidObserver mUidObserver;
+
+    private final NativeWrapper mNativeWrapper;
+
+    @VisibleForTesting final IHintManager.Stub mService = new BinderService();
+
+    public HintManagerService(Context context) {
+        this(context, new Injector());
+    }
+
+    @VisibleForTesting
+    HintManagerService(Context context, Injector injector) {
+        super(context);
+        mActiveSessions = new ArrayMap<>();
+        mNativeWrapper = injector.createNativeWrapper();
+        mNativeWrapper.halInit();
+        mHintSessionPreferredRate = mNativeWrapper.halGetHintSessionPreferredRate();
+        mUidObserver = new UidObserver();
+    }
+
+    @VisibleForTesting
+    static class Injector {
+        NativeWrapper createNativeWrapper() {
+            return new NativeWrapper();
+        }
+    }
+
+    private boolean isHalSupported() {
+        return mHintSessionPreferredRate != -1;
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(Context.PERFORMANCE_HINT_SERVICE, mService, /* allowIsolated= */ true);
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+            systemReady();
+        }
+    }
+
+    private void systemReady() {
+        Slogf.v(TAG, "Initializing HintManager service...");
+        try {
+            ActivityManager.getService().registerUidObserver(mUidObserver,
+                    ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE,
+                    ActivityManager.PROCESS_STATE_UNKNOWN, null);
+        } catch (RemoteException e) {
+            // ignored; both services live in system_server
+        }
+
+    }
+
+    /**
+     * Wrapper around the static-native methods from native.
+     *
+     * This class exists to allow us to mock static native methods in our tests. If mocking static
+     * methods becomes easier than this in the future, we can delete this class.
+     */
+    @VisibleForTesting
+    public static class NativeWrapper {
+        private native void nativeInit();
+
+        private static native long nativeCreateHintSession(int tgid, int uid, int[] tids,
+                long durationNanos);
+
+        private static native void nativePauseHintSession(long halPtr);
+
+        private static native void nativeResumeHintSession(long halPtr);
+
+        private static native void nativeCloseHintSession(long halPtr);
+
+        private static native void nativeUpdateTargetWorkDuration(
+                long halPtr, long targetDurationNanos);
+
+        private static native void nativeReportActualWorkDuration(
+                long halPtr, long[] actualDurationNanos, long[] timeStampNanos);
+
+        private static native long nativeGetHintSessionPreferredRate();
+
+        /** Wrapper for HintManager.nativeInit */
+        public void halInit() {
+            nativeInit();
+        }
+
+        /** Wrapper for HintManager.nativeCreateHintSession */
+        public long halCreateHintSession(int tgid, int uid, int[] tids, long durationNanos) {
+            return nativeCreateHintSession(tgid, uid, tids, durationNanos);
+        }
+
+        /** Wrapper for HintManager.nativePauseHintSession */
+        public void halPauseHintSession(long halPtr) {
+            nativePauseHintSession(halPtr);
+        }
+
+        /** Wrapper for HintManager.nativeResumeHintSession */
+        public void halResumeHintSession(long halPtr) {
+            nativeResumeHintSession(halPtr);
+        }
+
+        /** Wrapper for HintManager.nativeCloseHintSession */
+        public void halCloseHintSession(long halPtr) {
+            nativeCloseHintSession(halPtr);
+        }
+
+        /** Wrapper for HintManager.nativeUpdateTargetWorkDuration */
+        public void halUpdateTargetWorkDuration(long halPtr, long targetDurationNanos) {
+            nativeUpdateTargetWorkDuration(halPtr, targetDurationNanos);
+        }
+
+        /** Wrapper for HintManager.nativeReportActualWorkDuration */
+        public void halReportActualWorkDuration(
+                long halPtr, long[] actualDurationNanos, long[] timeStampNanos) {
+            nativeReportActualWorkDuration(halPtr, actualDurationNanos,
+                    timeStampNanos);
+        }
+
+        /** Wrapper for HintManager.nativeGetHintSessionPreferredRate */
+        public long halGetHintSessionPreferredRate() {
+            return nativeGetHintSessionPreferredRate();
+        }
+    }
+
+    @VisibleForTesting
+    final class UidObserver extends IUidObserver.Stub {
+        private final SparseArray<Integer> mProcStatesCache = new SparseArray<>();
+
+        public boolean isUidForeground(int uid) {
+            synchronized (mLock) {
+                return mProcStatesCache.get(uid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND)
+                        <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
+            }
+        }
+
+        @Override
+        public void onUidGone(int uid, boolean disabled) {
+            FgThread.getHandler().post(() -> {
+                synchronized (mLock) {
+                    ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(uid);
+                    if (tokenMap == null) {
+                        return;
+                    }
+                    for (int i = tokenMap.size() - 1; i >= 0; i--) {
+                        // Will remove the session from tokenMap
+                        tokenMap.valueAt(i).close();
+                    }
+                    mProcStatesCache.delete(uid);
+                }
+            });
+        }
+
+        @Override
+        public void onUidActive(int uid) {
+        }
+
+        @Override
+        public void onUidIdle(int uid, boolean disabled) {
+        }
+
+        /**
+         * The IUidObserver callback is called from the system_server, so it'll be a direct function
+         * call from ActivityManagerService. Do not do heavy logic here.
+         */
+        @Override
+        public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
+            FgThread.getHandler().post(() -> {
+                synchronized (mLock) {
+                    mProcStatesCache.put(uid, procState);
+                    ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(uid);
+                    if (tokenMap == null) {
+                        return;
+                    }
+                    for (AppHintSession s : tokenMap.values()) {
+                        s.onProcStateChanged();
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onUidCachedChanged(int uid, boolean cached) {
+        }
+    }
+
+    @VisibleForTesting
+    IHintManager.Stub getBinderServiceInstance() {
+        return mService;
+    }
+
+    private boolean checkTidValid(int tgid, int [] tids) {
+        // Make sure all tids belongs to the same process.
+        for (int threadId : tids) {
+            if (!Process.isThreadInProcess(tgid, threadId)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @VisibleForTesting
+    final class BinderService extends IHintManager.Stub {
+        @Override
+        public IHintSession createHintSession(IBinder token, int[] tids, long durationNanos) {
+            if (!isHalSupported()) return null;
+
+            java.util.Objects.requireNonNull(token);
+            java.util.Objects.requireNonNull(tids);
+            Preconditions.checkArgument(tids.length != 0, "tids should"
+                    + " not be empty.");
+
+            int uid = Binder.getCallingUid();
+            int tid = Binder.getCallingPid();
+            int pid = Process.getThreadGroupLeader(tid);
+
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                if (!checkTidValid(pid, tids)) {
+                    throw new SecurityException("Some tid doesn't belong to the process");
+                }
+
+                long halSessionPtr = mNativeWrapper.halCreateHintSession(pid, uid, tids,
+                        durationNanos);
+                if (halSessionPtr == 0) return null;
+
+                AppHintSession hs = new AppHintSession(uid, pid, tids, token,
+                        halSessionPtr, durationNanos);
+                synchronized (mLock) {
+                    ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(uid);
+                    if (tokenMap == null) {
+                        tokenMap = new ArrayMap<>(1);
+                        mActiveSessions.put(uid, tokenMap);
+                    }
+                    tokenMap.put(token, hs);
+                    return hs;
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public long getHintSessionPreferredRate() {
+            return mHintSessionPreferredRate;
+        }
+
+        @Override
+        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
+                return;
+            }
+            synchronized (mLock) {
+                pw.println("HintSessionPreferredRate: " + mHintSessionPreferredRate);
+                pw.println("HAL Support: " + isHalSupported());
+                pw.println("Active Sessions:");
+                for (int i = 0; i < mActiveSessions.size(); i++) {
+                    pw.println("Uid " + mActiveSessions.keyAt(i).toString() + ":");
+                    ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.valueAt(i);
+                    for (int j = 0; j < tokenMap.size(); j++) {
+                        pw.println("  Session " + j + ":");
+                        tokenMap.valueAt(j).dump(pw, "    ");
+                    }
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    final class AppHintSession extends IHintSession.Stub implements IBinder.DeathRecipient {
+        protected final int mUid;
+        protected final int mPid;
+        protected final int[] mThreadIds;
+        protected final IBinder mToken;
+        protected long mHalSessionPtr;
+        protected long mTargetDurationNanos;
+        protected boolean mUpdateAllowed;
+
+        protected AppHintSession(
+                int uid, int pid, int[] threadIds, IBinder token,
+                long halSessionPtr, long durationNanos) {
+            mUid = uid;
+            mPid = pid;
+            mToken = token;
+            mThreadIds = threadIds;
+            mHalSessionPtr = halSessionPtr;
+            mTargetDurationNanos = durationNanos;
+            mUpdateAllowed = true;
+            updateHintAllowed();
+            try {
+                token.linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                mNativeWrapper.halCloseHintSession(mHalSessionPtr);
+                throw new IllegalStateException("Client already dead", e);
+            }
+        }
+
+        @VisibleForTesting
+        boolean updateHintAllowed() {
+            synchronized (mLock) {
+                final boolean allowed = mUidObserver.isUidForeground(mUid);
+                if (allowed && !mUpdateAllowed) resume();
+                if (!allowed && mUpdateAllowed) pause();
+                mUpdateAllowed = allowed;
+                return mUpdateAllowed;
+            }
+        }
+
+        @Override
+        public void updateTargetWorkDuration(long targetDurationNanos) {
+            synchronized (mLock) {
+                if (mHalSessionPtr == 0 || !updateHintAllowed()) {
+                    return;
+                }
+                Preconditions.checkArgument(targetDurationNanos > 0, "Expected"
+                        + " the target duration to be greater than 0.");
+                mNativeWrapper.halUpdateTargetWorkDuration(mHalSessionPtr, targetDurationNanos);
+                mTargetDurationNanos = targetDurationNanos;
+            }
+        }
+
+        @Override
+        public void reportActualWorkDuration(long[] actualDurationNanos, long[] timeStampNanos) {
+            synchronized (mLock) {
+                if (mHalSessionPtr == 0 || !updateHintAllowed()) {
+                    return;
+                }
+                Preconditions.checkArgument(actualDurationNanos.length != 0, "the count"
+                        + " of hint durations shouldn't be 0.");
+                Preconditions.checkArgument(actualDurationNanos.length == timeStampNanos.length,
+                        "The length of durations and timestamps should be the same.");
+                for (int i = 0; i < actualDurationNanos.length; i++) {
+                    if (actualDurationNanos[i] <= 0) {
+                        throw new IllegalArgumentException(
+                                String.format("durations[%d]=%d should be greater than 0",
+                                        i, actualDurationNanos[i]));
+                    }
+                }
+                mNativeWrapper.halReportActualWorkDuration(mHalSessionPtr, actualDurationNanos,
+                        timeStampNanos);
+            }
+        }
+
+        /** TODO: consider monitor session threads and close session if any thread is dead. */
+        @Override
+        public void close() {
+            synchronized (mLock) {
+                if (mHalSessionPtr == 0) return;
+                mNativeWrapper.halCloseHintSession(mHalSessionPtr);
+                mHalSessionPtr = 0;
+                mToken.unlinkToDeath(this, 0);
+                ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(mUid);
+                if (tokenMap == null) {
+                    Slogf.w(TAG, "UID %d is note present in active session map", mUid);
+                }
+                tokenMap.remove(mToken);
+                if (tokenMap.isEmpty()) mActiveSessions.remove(mUid);
+            }
+        }
+
+        private void onProcStateChanged() {
+            updateHintAllowed();
+        }
+
+        private void pause() {
+            synchronized (mLock) {
+                if (mHalSessionPtr == 0) return;
+                mNativeWrapper.halPauseHintSession(mHalSessionPtr);
+            }
+        }
+
+        private void resume() {
+            synchronized (mLock) {
+                if (mHalSessionPtr == 0) return;
+                mNativeWrapper.halResumeHintSession(mHalSessionPtr);
+            }
+        }
+
+        private void dump(PrintWriter pw, String prefix) {
+            synchronized (mLock) {
+                pw.println(prefix + "SessionPID: " + mPid);
+                pw.println(prefix + "SessionUID: " + mUid);
+                pw.println(prefix + "SessionTIDs: " + Arrays.toString(mThreadIds));
+                pw.println(prefix + "SessionTargetDurationNanos: " + mTargetDurationNanos);
+                pw.println(prefix + "SessionAllowed: " + updateHintAllowed());
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            close();
+        }
+
+    }
+}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 15f5765..a99679a 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -49,6 +49,7 @@
         "com_android_server_net_NetworkStatsService.cpp",
         "com_android_server_power_PowerManagerService.cpp",
         "com_android_server_powerstats_PowerStatsService.cpp",
+        "com_android_server_hint_HintManagerService.cpp",
         "com_android_server_SerialService.cpp",
         "com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp",
         "com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker.cpp",
@@ -158,7 +159,7 @@
         "android.hardware.memtrack-V1-ndk_platform",
         "android.hardware.power@1.0",
         "android.hardware.power@1.1",
-        "android.hardware.power-V1-cpp",
+        "android.hardware.power-V2-cpp",
         "android.hardware.power.stats@1.0",
         "android.hardware.power.stats-V1-ndk_platform",
         "android.hardware.thermal@1.0",
@@ -195,8 +196,8 @@
                 "libchrome",
                 "libmojo",
             ],
-        }
-    }
+        },
+    },
 }
 
 filegroup {
diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp
new file mode 100644
index 0000000..000cb83
--- /dev/null
+++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#define TAG "HintManagerService-JNI"
+
+//#define LOG_NDEBUG 0
+
+#include <android-base/stringprintf.h>
+#include <android/hardware/power/IPower.h>
+#include <android_runtime/AndroidRuntime.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
+#include <powermanager/PowerHalController.h>
+#include <utils/Log.h>
+
+#include <unistd.h>
+#include <cinttypes>
+
+#include <sys/types.h>
+
+#include "jni.h"
+
+using android::hardware::power::IPowerHintSession;
+using android::hardware::power::WorkDuration;
+
+using android::base::StringPrintf;
+
+namespace android {
+
+static power::PowerHalController gPowerHalController;
+
+static jlong createHintSession(JNIEnv* env, int32_t tgid, int32_t uid,
+                               std::vector<int32_t> threadIds, int64_t durationNanos) {
+    auto result =
+            gPowerHalController.createHintSession(tgid, uid, std::move(threadIds), durationNanos);
+    if (result.isOk()) {
+        sp<IPowerHintSession> appSession = result.value();
+        if (appSession) appSession->incStrong(env);
+        return reinterpret_cast<jlong>(appSession.get());
+    }
+    return 0;
+}
+
+static void pauseHintSession(JNIEnv* env, int64_t session_ptr) {
+    sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    appSession->pause();
+}
+
+static void resumeHintSession(JNIEnv* env, int64_t session_ptr) {
+    sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    appSession->resume();
+}
+
+static void closeHintSession(JNIEnv* env, int64_t session_ptr) {
+    sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    appSession->close();
+    appSession->decStrong(env);
+}
+
+static void updateTargetWorkDuration(int64_t session_ptr, int64_t targetDurationNanos) {
+    sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    appSession->updateTargetWorkDuration(targetDurationNanos);
+}
+
+static void reportActualWorkDuration(int64_t session_ptr,
+                                     const std::vector<WorkDuration>& actualDurations) {
+    sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    appSession->reportActualWorkDuration(actualDurations);
+}
+
+static int64_t getHintSessionPreferredRate() {
+    int64_t rate = -1;
+    auto result = gPowerHalController.getHintSessionPreferredRate();
+    if (result.isOk()) {
+        rate = result.value();
+    }
+    return rate;
+}
+
+// ----------------------------------------------------------------------------
+static void nativeInit(JNIEnv* env, jobject obj) {
+    gPowerHalController.init();
+}
+
+static jlong nativeCreateHintSession(JNIEnv* env, jclass /* clazz */, jint tgid, jint uid,
+                                     jintArray tids, jlong durationNanos) {
+    ScopedIntArrayRO tidArray(env, tids);
+    if (nullptr == tidArray.get() || tidArray.size() == 0) {
+        ALOGW("GetIntArrayElements returns nullptr.");
+        return 0;
+    }
+    std::vector<int32_t> threadIds(tidArray.size());
+    for (size_t i = 0; i < tidArray.size(); i++) {
+        threadIds[i] = tidArray[i];
+    }
+    return createHintSession(env, tgid, uid, std::move(threadIds), durationNanos);
+}
+
+static void nativePauseHintSession(JNIEnv* env, jclass /* clazz */, jlong session_ptr) {
+    pauseHintSession(env, session_ptr);
+}
+
+static void nativeResumeHintSession(JNIEnv* env, jclass /* clazz */, jlong session_ptr) {
+    resumeHintSession(env, session_ptr);
+}
+
+static void nativeCloseHintSession(JNIEnv* env, jclass /* clazz */, jlong session_ptr) {
+    closeHintSession(env, session_ptr);
+}
+
+static void nativeUpdateTargetWorkDuration(JNIEnv* /* env */, jclass /* clazz */, jlong session_ptr,
+                                           jlong targetDurationNanos) {
+    updateTargetWorkDuration(session_ptr, targetDurationNanos);
+}
+
+static void nativeReportActualWorkDuration(JNIEnv* env, jclass /* clazz */, jlong session_ptr,
+                                           jlongArray actualDurations, jlongArray timeStamps) {
+    ScopedLongArrayRO arrayActualDurations(env, actualDurations);
+    ScopedLongArrayRO arrayTimeStamps(env, timeStamps);
+
+    std::vector<WorkDuration> actualList(arrayActualDurations.size());
+    for (size_t i = 0; i < arrayActualDurations.size(); i++) {
+        actualList[i].timeStampNanos = arrayTimeStamps[i];
+        actualList[i].durationNanos = arrayActualDurations[i];
+    }
+    reportActualWorkDuration(session_ptr, actualList);
+}
+
+static jlong nativeGetHintSessionPreferredRate(JNIEnv* /* env */, jclass /* clazz */) {
+    return static_cast<jlong>(getHintSessionPreferredRate());
+}
+
+// ----------------------------------------------------------------------------
+static const JNINativeMethod sHintManagerServiceMethods[] = {
+        /* name, signature, funcPtr */
+        {"nativeInit", "()V", (void*)nativeInit},
+        {"nativeCreateHintSession", "(II[IJ)J", (void*)nativeCreateHintSession},
+        {"nativePauseHintSession", "(J)V", (void*)nativePauseHintSession},
+        {"nativeResumeHintSession", "(J)V", (void*)nativeResumeHintSession},
+        {"nativeCloseHintSession", "(J)V", (void*)nativeCloseHintSession},
+        {"nativeUpdateTargetWorkDuration", "(JJ)V", (void*)nativeUpdateTargetWorkDuration},
+        {"nativeReportActualWorkDuration", "(J[J[J)V", (void*)nativeReportActualWorkDuration},
+        {"nativeGetHintSessionPreferredRate", "()J", (void*)nativeGetHintSessionPreferredRate},
+};
+
+int register_android_server_HintManagerService(JNIEnv* env) {
+    return jniRegisterNativeMethods(env,
+                                    "com/android/server/power/hint/"
+                                    "HintManagerService$NativeWrapper",
+                                    sHintManagerServiceMethods, NELEM(sHintManagerServiceMethods));
+}
+
+} /* namespace android */
diff --git a/services/core/jni/com_android_server_power_PowerManagerService.cpp b/services/core/jni/com_android_server_power_PowerManagerService.cpp
index 9b7e27d..ae7ea3c 100644
--- a/services/core/jni/com_android_server_power_PowerManagerService.cpp
+++ b/services/core/jni/com_android_server_power_PowerManagerService.cpp
@@ -100,7 +100,7 @@
         ALOGD("Excessive delay in setting interactive mode to %s while turning screen %s",
               enabled ? "true" : "false", enabled ? "on" : "off");
     }
-    return result == power::HalResult::SUCCESSFUL;
+    return result.isOk();
 }
 
 void android_server_PowerManagerService_userActivity(nsecs_t eventTime, int32_t eventType,
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 03a0152..f257686 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -30,6 +30,7 @@
 int register_android_server_LightsService(JNIEnv* env);
 int register_android_server_PowerManagerService(JNIEnv* env);
 int register_android_server_PowerStatsService(JNIEnv* env);
+int register_android_server_HintManagerService(JNIEnv* env);
 int register_android_server_storage_AppFuse(JNIEnv* env);
 int register_android_server_SerialService(JNIEnv* env);
 int register_android_server_SystemServer(JNIEnv* env);
@@ -79,6 +80,7 @@
     register_android_server_broadcastradio_Tuner(vm, env);
     register_android_server_PowerManagerService(env);
     register_android_server_PowerStatsService(env);
+    register_android_server_HintManagerService(env);
     register_android_server_SerialService(env);
     register_android_server_InputManager(env);
     register_android_server_LightsService(env);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 1426579..47e72ba 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -170,6 +170,7 @@
 import com.android.server.power.PowerManagerService;
 import com.android.server.power.ShutdownThread;
 import com.android.server.power.ThermalManagerService;
+import com.android.server.power.hint.HintManagerService;
 import com.android.server.powerstats.PowerStatsService;
 import com.android.server.profcollect.ProfcollectForwardingService;
 import com.android.server.recoverysystem.RecoverySystemService;
@@ -1074,6 +1075,10 @@
         mSystemServiceManager.startService(ThermalManagerService.class);
         t.traceEnd();
 
+        t.traceBegin("StartHintManager");
+        mSystemServiceManager.startService(HintManagerService.class);
+        t.traceEnd();
+
         // Now that the power manager has been started, let the activity manager
         // initialize power management features.
         t.traceBegin("InitPowerManagement");
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
new file mode 100644
index 0000000..aaf40d7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2021 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.power.hint;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IHintSession;
+import android.os.Process;
+
+import com.android.server.FgThread;
+import com.android.server.power.hint.HintManagerService.AppHintSession;
+import com.android.server.power.hint.HintManagerService.Injector;
+import com.android.server.power.hint.HintManagerService.NativeWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link com.android.server.power.hint.HintManagerService}.
+ *
+ * Build/Install/Run:
+ *  atest FrameworksServicesTests:HintManagerServiceTest
+ */
+public class HintManagerServiceTest {
+    private static final long DEFAULT_HINT_PREFERRED_RATE = 16666666L;
+    private static final long DEFAULT_TARGET_DURATION = 16666666L;
+    private static final int UID = Process.myUid();
+    private static final int TID = Process.myPid();
+    private static final int TGID = Process.getThreadGroupLeader(TID);
+    private static final int[] SESSION_TIDS_A = new int[] {TID};
+    private static final int[] SESSION_TIDS_B = new int[] {TID};
+    private static final long[] DURATIONS_THREE = new long[] {1L, 100L, 1000L};
+    private static final long[] TIMESTAMPS_THREE = new long[] {1L, 2L, 3L};
+    private static final long[] DURATIONS_ZERO = new long[] {};
+    private static final long[] TIMESTAMPS_ZERO = new long[] {};
+    private static final long[] TIMESTAMPS_TWO = new long[] {1L, 2L};
+
+    @Mock private Context mContext;
+    @Mock private HintManagerService.NativeWrapper mNativeWrapperMock;
+
+    private HintManagerService mService;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        when(mNativeWrapperMock.halGetHintSessionPreferredRate())
+                .thenReturn(DEFAULT_HINT_PREFERRED_RATE);
+        when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A),
+              eq(DEFAULT_TARGET_DURATION))).thenReturn(1L);
+        when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_B),
+              eq(DEFAULT_TARGET_DURATION))).thenReturn(2L);
+    }
+
+    private HintManagerService createService() {
+        mService = new HintManagerService(mContext, new Injector() {
+            NativeWrapper createNativeWrapper() {
+                return mNativeWrapperMock;
+            }
+        });
+        return mService;
+    }
+
+    @Test
+    public void testInitializeService() {
+        HintManagerService service = createService();
+        verify(mNativeWrapperMock).halInit();
+        assertThat(service.mHintSessionPreferredRate).isEqualTo(DEFAULT_HINT_PREFERRED_RATE);
+    }
+
+    @Test
+    public void testCreateHintSession() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+
+        IHintSession a = service.getBinderServiceInstance().createHintSession(token,
+                SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+        assertNotNull(a);
+
+        IHintSession b = service.getBinderServiceInstance().createHintSession(token,
+                SESSION_TIDS_B, DEFAULT_TARGET_DURATION);
+        assertNotEquals(a, b);
+    }
+
+    @Test
+    public void testPauseResumeHintSession() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+
+        AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
+        // Set session to background and calling updateHintAllowed() would invoke pause();
+        service.mUidObserver.onUidStateChanged(
+                a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        final Object sync = new Object();
+        FgThread.getHandler().post(() -> {
+            synchronized (sync) {
+                sync.notify();
+            }
+        });
+        synchronized (sync) {
+            sync.wait();
+        }
+        assumeFalse(a.updateHintAllowed());
+        verify(mNativeWrapperMock, times(1)).halPauseHintSession(anyLong());
+
+        // Set session to foreground and calling updateHintAllowed() would invoke resume();
+        service.mUidObserver.onUidStateChanged(
+                a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+        FgThread.getHandler().post(() -> {
+            synchronized (sync) {
+                sync.notify();
+            }
+        });
+        synchronized (sync) {
+            sync.wait();
+        }
+        assumeTrue(a.updateHintAllowed());
+        verify(mNativeWrapperMock, times(1)).halResumeHintSession(anyLong());
+    }
+
+    @Test
+    public void testCloseHintSession() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+
+        IHintSession a = service.getBinderServiceInstance().createHintSession(token,
+                SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
+        a.close();
+        verify(mNativeWrapperMock, times(1)).halCloseHintSession(anyLong());
+    }
+
+    @Test
+    public void testUpdateTargetWorkDuration() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+
+        IHintSession a = service.getBinderServiceInstance().createHintSession(token,
+                SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            a.updateTargetWorkDuration(-1L);
+        });
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            a.updateTargetWorkDuration(0L);
+        });
+
+        a.updateTargetWorkDuration(100L);
+        verify(mNativeWrapperMock, times(1)).halUpdateTargetWorkDuration(anyLong(), eq(100L));
+    }
+
+    @Test
+    public void testReportActualWorkDuration() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+
+        AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
+        a.updateTargetWorkDuration(100L);
+        a.reportActualWorkDuration(DURATIONS_THREE, TIMESTAMPS_THREE);
+        verify(mNativeWrapperMock, times(1)).halReportActualWorkDuration(anyLong(),
+                eq(DURATIONS_THREE), eq(TIMESTAMPS_THREE));
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            a.reportActualWorkDuration(DURATIONS_ZERO, TIMESTAMPS_THREE);
+        });
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            a.reportActualWorkDuration(DURATIONS_THREE, TIMESTAMPS_ZERO);
+        });
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            a.reportActualWorkDuration(DURATIONS_THREE, TIMESTAMPS_TWO);
+        });
+
+        reset(mNativeWrapperMock);
+        // Set session to background, then the duration would not be updated.
+        service.mUidObserver.onUidStateChanged(
+                a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        final Object sync = new Object();
+        FgThread.getHandler().post(() -> {
+            synchronized (sync) {
+                sync.notify();
+            }
+        });
+        synchronized (sync) {
+            sync.wait();
+        }
+        assumeFalse(a.updateHintAllowed());
+        a.reportActualWorkDuration(DURATIONS_THREE, TIMESTAMPS_THREE);
+        verify(mNativeWrapperMock, never()).halReportActualWorkDuration(anyLong(), any(), any());
+    }
+
+    @Test
+    public void testDoHintInBackground() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+
+        AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
+        service.mUidObserver.onUidStateChanged(
+                a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
+        final Object sync = new Object();
+        FgThread.getHandler().post(() -> {
+            synchronized (sync) {
+                sync.notify();
+            }
+        });
+        synchronized (sync) {
+            sync.wait();
+        }
+        assertFalse(a.updateHintAllowed());
+    }
+
+    @Test
+    public void testDoHintInForeground() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+
+        AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
+        service.mUidObserver.onUidStateChanged(
+                a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+        assertTrue(a.updateHintAllowed());
+    }
+}