Add mic mute keyboard led. (1/2)

Change-Id: Ie3ddd04853bb3471bb60cefd037d5244733046c4
Test: Built, presubmit
diff --git a/core/java/android/hardware/lights/Light.java b/core/java/android/hardware/lights/Light.java
index 18d0b09..163f9fa 100644
--- a/core/java/android/hardware/lights/Light.java
+++ b/core/java/android/hardware/lights/Light.java
@@ -65,6 +65,12 @@
     public static final int LIGHT_TYPE_KEYBOARD_BACKLIGHT = 10003;
 
     /**
+     * Type for keyboard microphone mute light.
+     * @hide
+     */
+    public static final int LIGHT_TYPE_KEYBOARD_MIC_MUTE = 10004;
+
+    /**
      * Capability for lights that could adjust its LED brightness. If the capability is not present
      * the LED can only be turned either on or off.
      */
@@ -92,6 +98,7 @@
             LIGHT_TYPE_INPUT,
             LIGHT_TYPE_PLAYER_ID,
             LIGHT_TYPE_KEYBOARD_BACKLIGHT,
+            LIGHT_TYPE_KEYBOARD_MIC_MUTE,
         })
     public @interface LightType {}
 
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index f32c11d..2ccc451 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -319,6 +319,9 @@
     // Manages Sticky modifier state
     private final StickyModifierStateController mStickyModifierStateController;
 
+    // Manages Keyboard microphone mute led
+    private final KeyboardLedController mKeyboardLedController;
+
     // Manages Keyboard modifier keys remapping
     private final KeyRemapper mKeyRemapper;
 
@@ -468,6 +471,8 @@
                         injector.getLooper(), injector.getUEventManager())
                 : new KeyboardBacklightControllerInterface() {};
         mStickyModifierStateController = new StickyModifierStateController();
+        mKeyboardLedController = new KeyboardLedController(mContext, injector.getLooper(),
+                mNative);
         mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
         mPointerIconCache = new PointerIconCache(mContext, mNative);
 
@@ -582,6 +587,7 @@
         mKeyboardLayoutManager.systemRunning();
         mBatteryController.systemRunning();
         mKeyboardBacklightController.systemRunning();
+        mKeyboardLedController.systemRunning();
         mKeyRemapper.systemRunning();
         mPointerIconCache.systemRunning();
     }
@@ -2164,6 +2170,7 @@
         dumpDisplayInputPropertiesValues(ipw);
         mBatteryController.dump(ipw);
         mKeyboardBacklightController.dump(ipw);
+        mKeyboardLedController.dump(ipw);
     }
 
     private void dumpAssociations(IndentingPrintWriter pw) {
diff --git a/services/core/java/com/android/server/input/KeyboardLedController.java b/services/core/java/com/android/server/input/KeyboardLedController.java
new file mode 100644
index 0000000..5c404a2
--- /dev/null
+++ b/services/core/java/com/android/server/input/KeyboardLedController.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2024 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.input;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Color;
+import android.hardware.SensorPrivacyManager;
+import android.hardware.SensorPrivacyManager.Sensors;
+import android.hardware.input.InputManager;
+import android.hardware.lights.Light;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.util.IndentingPrintWriter;
+import android.util.SparseArray;
+import android.view.InputDevice;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+
+/**
+ * This class is used to control the light of keyboard.
+ */
+public final class KeyboardLedController implements InputManager.InputDeviceListener {
+
+    private static final String TAG = KeyboardLedController.class.getSimpleName();
+    private static final int MSG_UPDATE_EXISTING_DEVICES = 1;
+    private static final int MSG_UPDATE_MIC_MUTE_LED_STATE = 2;
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final NativeInputManagerService mNative;
+    private final SparseArray<InputDevice> mKeyboardsWithMicMuteLed = new SparseArray<>();
+    @NonNull
+    private InputManager mInputManager;
+    @NonNull
+    private SensorPrivacyManager mSensorPrivacyManager;
+    @NonNull
+    private AudioManager mAudioManager;
+    private BroadcastReceiver mMicrophoneMuteChangedIntentReceiver =
+            new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    Message msg =  Message.obtain(mHandler, MSG_UPDATE_MIC_MUTE_LED_STATE);
+                    mHandler.sendMessage(msg);
+                }
+            };
+
+    KeyboardLedController(Context context, Looper looper,
+            NativeInputManagerService nativeService) {
+        mContext = context;
+        mNative = nativeService;
+        mHandler = new Handler(looper, this::handleMessage);
+    }
+
+    private boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_UPDATE_EXISTING_DEVICES:
+                for (int deviceId : (int[]) msg.obj) {
+                    onInputDeviceAdded(deviceId);
+                }
+                return true;
+            case MSG_UPDATE_MIC_MUTE_LED_STATE:
+                updateMicMuteLedState();
+                return true;
+        }
+        return false;
+    }
+
+    private void updateMicMuteLedState() {
+        // We determine if the microphone is muted by querying both the hardware state of the
+        // microphone and the microphone sensor privacy hardware and sensor toggles
+        boolean isMicrophoneMute = mAudioManager.isMicrophoneMute()
+                || mSensorPrivacyManager.areAnySensorPrivacyTogglesEnabled(Sensors.MICROPHONE);
+        int color = isMicrophoneMute ? Color.WHITE : Color.TRANSPARENT;
+        for (int i = 0; i < mKeyboardsWithMicMuteLed.size(); i++) {
+            InputDevice device = mKeyboardsWithMicMuteLed.valueAt(i);
+            if (device != null) {
+                int deviceId = device.getId();
+                Light light = getKeyboardMicMuteLight(device);
+                if (light != null) {
+                    mNative.setLightColor(deviceId, light.getId(), color);
+                }
+            }
+        }
+    }
+
+    private Light getKeyboardMicMuteLight(InputDevice device) {
+        for (Light light : device.getLightsManager().getLights()) {
+            if (light.getType() == Light.LIGHT_TYPE_KEYBOARD_MIC_MUTE
+                    && light.hasBrightnessControl()) {
+                return light;
+            }
+        }
+        return null;
+    }
+
+    /** Called when the system is ready for us to start third-party code. */
+    public void systemRunning() {
+        mSensorPrivacyManager = Objects.requireNonNull(
+                mContext.getSystemService(SensorPrivacyManager.class));
+        mInputManager = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+        mAudioManager = Objects.requireNonNull(mContext.getSystemService(AudioManager.class));
+        mInputManager.registerInputDeviceListener(this, mHandler);
+        Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES,
+                mInputManager.getInputDeviceIds());
+        mHandler.sendMessage(msg);
+        mContext.registerReceiverAsUser(
+                mMicrophoneMuteChangedIntentReceiver,
+                UserHandle.ALL,
+                new IntentFilter(AudioManager.ACTION_MICROPHONE_MUTE_CHANGED),
+                null,
+                mHandler);
+    }
+
+    @Override
+    public void onInputDeviceAdded(int deviceId) {
+        onInputDeviceChanged(deviceId);
+    }
+
+    @Override
+    public void onInputDeviceRemoved(int deviceId) {
+        mKeyboardsWithMicMuteLed.remove(deviceId);
+    }
+
+    @Override
+    public void onInputDeviceChanged(int deviceId) {
+        InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
+        if (inputDevice == null) {
+            return;
+        }
+        if (getKeyboardMicMuteLight(inputDevice) != null) {
+            mKeyboardsWithMicMuteLed.put(deviceId, inputDevice);
+            Message msg = Message.obtain(mHandler, MSG_UPDATE_MIC_MUTE_LED_STATE);
+            mHandler.sendMessage(msg);
+        }
+    }
+
+    /** Dump the diagnostic information */
+    public void dump(PrintWriter pw) {
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+        ipw.println(TAG + ": " + mKeyboardsWithMicMuteLed.size() + " keyboard mic mute lights");
+        ipw.increaseIndent();
+        for (int i = 0; i < mKeyboardsWithMicMuteLed.size(); i++) {
+            InputDevice inputDevice = mKeyboardsWithMicMuteLed.valueAt(i);
+            ipw.println(i + " " + inputDevice.getName() + ": "
+                    + getKeyboardMicMuteLight(inputDevice).toString());
+        }
+        ipw.decreaseIndent();
+    }
+}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 88c47f3..5b4f47e 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -178,6 +178,7 @@
     jfieldID lightTypeInput;
     jfieldID lightTypePlayerId;
     jfieldID lightTypeKeyboardBacklight;
+    jfieldID lightTypeKeyboardMicMute;
     jfieldID lightCapabilityBrightness;
     jfieldID lightCapabilityColorRgb;
 } gLightClassInfo;
@@ -2444,6 +2445,9 @@
         } else if (lightInfo.type == InputDeviceLightType::KEYBOARD_BACKLIGHT) {
             jTypeId = env->GetStaticIntField(gLightClassInfo.clazz,
                                              gLightClassInfo.lightTypeKeyboardBacklight);
+        } else if (lightInfo.type == InputDeviceLightType::KEYBOARD_MIC_MUTE) {
+            jTypeId = env->GetStaticIntField(gLightClassInfo.clazz,
+                                             gLightClassInfo.lightTypeKeyboardMicMute);
         } else {
             ALOGW("Unknown light type %d", lightInfo.type);
             continue;
@@ -3176,6 +3180,8 @@
             env->GetStaticFieldID(gLightClassInfo.clazz, "LIGHT_TYPE_PLAYER_ID", "I");
     gLightClassInfo.lightTypeKeyboardBacklight =
             env->GetStaticFieldID(gLightClassInfo.clazz, "LIGHT_TYPE_KEYBOARD_BACKLIGHT", "I");
+    gLightClassInfo.lightTypeKeyboardMicMute =
+            env->GetStaticFieldID(gLightClassInfo.clazz, "LIGHT_TYPE_KEYBOARD_MIC_MUTE", "I");
     gLightClassInfo.lightCapabilityBrightness =
             env->GetStaticFieldID(gLightClassInfo.clazz, "LIGHT_CAPABILITY_BRIGHTNESS", "I");
     gLightClassInfo.lightCapabilityColorRgb =
diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp
index d17cd1f..a85d809 100644
--- a/tests/Input/Android.bp
+++ b/tests/Input/Android.bp
@@ -30,7 +30,7 @@
         "androidx.test.rules",
         "androidx.test.runner",
         "androidx.test.uiautomator_uiautomator",
-        "servicestests-utils",
+        "compatibility-device-util-axt",
         "flag-junit",
         "frameworks-base-testutils",
         "hamcrest-library",
@@ -38,6 +38,7 @@
         "mockito-target-minus-junit4",
         "platform-test-annotations",
         "services.core.unboosted",
+        "servicestests-utils",
         "testables",
         "testng",
         "truth",
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index f6f766a..2c53f8c 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -33,6 +33,7 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.internal.util.test.FakeSettingsProvider
 import com.google.common.truth.Truth.assertThat
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
 import org.junit.After
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -141,7 +142,9 @@
     fun testInputSettingsUpdatedOnSystemRunning() {
         verifyZeroInteractions(native)
 
-        service.systemRunning()
+        runWithShellPermissionIdentity {
+            service.systemRunning()
+        }
 
         verify(native).setPointerSpeed(anyInt())
         verify(native).setTouchpadPointerSpeed(anyInt())