Hide IME when the input target was covered by IME layering overlay

Bug: 258048231
Bug: 224195164
Bug: 252192121

Test: manual with issue steps
Test: atest ImeVisibilityStateComputerTest#\
         testOnApplyImeVisibilityFromComputer
Change-Id: Ia71b975898efb19439c3a1b1a9a2bdcf21b78650
diff --git a/core/java/com/android/internal/inputmethod/InputMethodDebug.java b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
index 1f93151..bb8bdf5 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodDebug.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
@@ -259,6 +259,8 @@
                 return "SHOW_IME_SCREENSHOT_FROM_IMMS";
             case SoftInputShowHideReason.REMOVE_IME_SCREENSHOT_FROM_IMMS:
                 return "REMOVE_IME_SCREENSHOT_FROM_IMMS";
+            case SoftInputShowHideReason.HIDE_WHEN_INPUT_TARGET_INVISIBLE:
+                return "HIDE_WHEN_INPUT_TARGET_INVISIBLE";
             default:
                 return "Unknown=" + reason;
         }
diff --git a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
index 4303491..6e9cd44 100644
--- a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
+++ b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
@@ -68,6 +68,7 @@
         SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR,
         SoftInputShowHideReason.SHOW_IME_SCREENSHOT_FROM_IMMS,
         SoftInputShowHideReason.REMOVE_IME_SCREENSHOT_FROM_IMMS,
+        SoftInputShowHideReason.HIDE_WHEN_INPUT_TARGET_INVISIBLE,
 })
 public @interface SoftInputShowHideReason {
     /** Show soft input by {@link android.view.inputmethod.InputMethodManager#showSoftInput}. */
@@ -271,4 +272,10 @@
      * Removes ime screenshot by {@link com.android.server.inputmethod.InputMethodManagerService}.
      */
     int REMOVE_IME_SCREENSHOT_FROM_IMMS = ImeProtoEnums.REASON_REMOVE_IME_SCREENSHOT_FROM_IMMS;
+
+    /**
+     * Hide soft input when the input target being removed or being obscured by an non-IME
+     * focusable overlay window.
+     */
+    int HIDE_WHEN_INPUT_TARGET_INVISIBLE = ImeProtoEnums.REASON_HIDE_WHEN_INPUT_TARGET_INVISIBLE;
 }
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index 46eee43..46d23bb 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -108,6 +108,12 @@
      */
     private boolean mRequestedImeScreenshot;
 
+    /** The window token of the current visible IME layering target overlay. */
+    private IBinder mCurVisibleImeLayeringOverlay;
+
+    /** The window token of the current visible IME input target. */
+    private IBinder mCurVisibleImeInputTarget;
+
     /** Represent the invalid IME visibility state */
     public static final int STATE_INVALID = -1;
 
@@ -190,13 +196,18 @@
             @Override
             public void onImeTargetOverlayVisibilityChanged(IBinder overlayWindowToken,
                     boolean visible, boolean removed) {
-                // TODO(b/258048231): implement logic to fix IME layering overlay visibility issue.
+                mCurVisibleImeLayeringOverlay = (visible && !removed) ? overlayWindowToken : null;
             }
 
             @Override
             public void onImeInputTargetVisibilityChanged(IBinder imeInputTarget,
                     boolean visibleRequested, boolean removed) {
-                // TODO(b/258048231): implement logic to fix IME input target visibility issue.
+                mCurVisibleImeInputTarget = (visibleRequested && !removed) ? imeInputTarget : null;
+                if (mCurVisibleImeInputTarget == null && mCurVisibleImeLayeringOverlay != null) {
+                    mService.onApplyImeVisibilityFromComputer(imeInputTarget,
+                            new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
+                                    SoftInputShowHideReason.HIDE_WHEN_INPUT_TARGET_INVISIBLE));
+                }
             }
         });
     }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index eafc7d7..9720ed6 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -4813,6 +4813,14 @@
         }
     }
 
+    void onApplyImeVisibilityFromComputer(IBinder windowToken,
+            @NonNull ImeVisibilityResult result) {
+        synchronized (ImfLock.class) {
+            mVisibilityApplier.applyImeVisibility(windowToken, null, result.getState(),
+                    result.getReason());
+        }
+    }
+
     @GuardedBy("ImfLock.class")
     void setEnabledSessionLocked(SessionState session) {
         if (mEnabledSession != session) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 9792c30..4f5083c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -3349,6 +3349,10 @@
     void dispatchImeTargetOverlayVisibilityChanged(@NonNull IBinder token, boolean visible,
             boolean removed) {
         if (mImeTargetChangeListener != null) {
+            if (DEBUG_INPUT_METHOD) {
+                Slog.d(TAG, "onImeTargetOverlayVisibilityChanged, win=" + mWindowMap.get(token)
+                        + "visible=" + visible + ", removed=" + removed);
+            }
             mH.post(() -> mImeTargetChangeListener.onImeTargetOverlayVisibilityChanged(token,
                     visible, removed));
         }
@@ -3357,6 +3361,10 @@
     void dispatchImeInputTargetVisibilityChanged(@NonNull IBinder token, boolean visible,
             boolean removed) {
         if (mImeTargetChangeListener != null) {
+            if (DEBUG_INPUT_METHOD) {
+                Slog.d(TAG, "onImeInputTargetVisibilityChanged, win=" + mWindowMap.get(token)
+                        + "visible=" + visible + ", removed=" + removed);
+            }
             mH.post(() -> mImeTargetChangeListener.onImeInputTargetVisibilityChanged(token,
                     visible, removed));
         }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
index a62222f..4e58907 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
@@ -24,9 +24,13 @@
 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_WHEN_INPUT_TARGET_INVISIBLE;
 import static com.android.internal.inputmethod.SoftInputShowHideReason.REMOVE_IME_SCREENSHOT_FROM_IMMS;
 import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_IME_SCREENSHOT_FROM_IMMS;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_REMOVE_IME_SNAPSHOT;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME_SNAPSHOT;
 import static com.android.server.inputmethod.InputMethodManagerService.FALLBACK_DISPLAY_ID;
@@ -41,11 +45,13 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+import com.android.server.wm.ImeTargetChangeListener;
 import com.android.server.wm.WindowManagerInternal;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 
 /**
  * Test the behavior of {@link ImeVisibilityStateComputer} and {@link ImeVisibilityApplier} when
@@ -57,6 +63,7 @@
 @RunWith(AndroidJUnit4.class)
 public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTestBase {
     private ImeVisibilityStateComputer mComputer;
+    private ImeTargetChangeListener mListener;
     private int mImeDisplayPolicy = DISPLAY_IME_POLICY_LOCAL;
 
     @Before
@@ -73,7 +80,11 @@
                 return displayId -> mImeDisplayPolicy;
             }
         };
+        ArgumentCaptor<ImeTargetChangeListener> captor = ArgumentCaptor.forClass(
+                ImeTargetChangeListener.class);
+        verify(mMockWindowManagerInternal).setInputMethodTargetChangeListener(captor.capture());
         mComputer = new ImeVisibilityStateComputer(mInputMethodManagerService, injector);
+        mListener = captor.getValue();
     }
 
     @Test
@@ -225,6 +236,28 @@
         assertThat(result.getReason()).isEqualTo(REMOVE_IME_SCREENSHOT_FROM_IMMS);
     }
 
+    @Test
+    public void testOnApplyImeVisibilityFromComputer() {
+        final IBinder testImeTargetOverlay = new Binder();
+        final IBinder testImeInputTarget = new Binder();
+
+        // Simulate a test IME layering target overlay fully occluded the IME input target.
+        mListener.onImeTargetOverlayVisibilityChanged(testImeTargetOverlay, true, false);
+        mListener.onImeInputTargetVisibilityChanged(testImeInputTarget, false, false);
+        final ArgumentCaptor<IBinder> targetCaptor = ArgumentCaptor.forClass(IBinder.class);
+        final ArgumentCaptor<ImeVisibilityResult> resultCaptor = ArgumentCaptor.forClass(
+                ImeVisibilityResult.class);
+        verify(mInputMethodManagerService).onApplyImeVisibilityFromComputer(targetCaptor.capture(),
+                resultCaptor.capture());
+        final IBinder imeInputTarget = targetCaptor.getValue();
+        final ImeVisibilityResult result = resultCaptor.getValue();
+
+        // Verify the computer will callback hiding IME state to IMMS.
+        assertThat(imeInputTarget).isEqualTo(testImeInputTarget);
+        assertThat(result.getState()).isEqualTo(STATE_HIDE_IME_EXPLICIT);
+        assertThat(result.getReason()).isEqualTo(HIDE_WHEN_INPUT_TARGET_INVISIBLE);
+    }
+
     private ImeTargetWindowState initImeTargetWindowState(IBinder windowToken) {
         final ImeTargetWindowState state = new ImeTargetWindowState(SOFT_INPUT_STATE_UNCHANGED,
                 0, true, true, true);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index 5319c3c..3a0b2ab 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -218,6 +218,7 @@
                         false);
         mInputMethodManagerService = new InputMethodManagerService(mContext, mServiceThread,
                 mMockInputMethodBindingController);
+        spyOn(mInputMethodManagerService);
 
         // Start a InputMethodManagerService.Lifecycle to publish and manage the lifecycle of
         // InputMethodManagerService, which is closer to the real situation.