Implement UX for IME with freeform windows

UX desires for this are: when IME appears for a freeform window,
1. Temporarily push the freeform window up to make room for IME
1a. However, do not push the top of the window off the screen
2. Any part of the window left under the IME becomes inset and
   thus handled by adjustPan or adjustResize.
3. Return the window to its original position when IME closes.
3a. Unless the window is moved while IME is up.
4. If the window is moved around while IME is up, do not change
   the content (ie. don't adjust insets).

This CL includes some fixes to related bugs as well:
- During adjustPan, the caption is now "unscrolled" so that
  it remains at the top of the window. Previously, the caption
  would be scrolled out of the window along with the content.
  This is done via setTranslation so it won't trigger relayout.
- The starting bounds of task-drag uses the task bounds instead
  of dim bounds. Dim bounds was based on the visible frame
  which excludes the IME inset; so, it was causing the window
  to be resized if the user tried to drag the window while IME
  was open. Going through the history, this was done to resolve
  some issue with resizing dialog activities. I've verified
  that behavior in this case is the same before and after this
  CL.

Bug: 119375946
Test: Manual since UX: open desktop display, open apps that
      use IME (both adjustPan and adjustResize).
      Also, atest WindowFrameTests
Change-Id: Id81d0b0a5f82be28fabed3ad22e713fc4fa7536d
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index cdaf33f..c4626c2 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -1639,6 +1639,9 @@
     @Override
     public void onRootViewScrollYChanged(int rootScrollY) {
         mRootScrollY = rootScrollY;
+        if (mDecorCaptionView != null) {
+            mDecorCaptionView.onRootViewScrollYChanged(rootScrollY);
+        }
         updateColorViewTranslations();
     }
 
diff --git a/core/java/com/android/internal/widget/DecorCaptionView.java b/core/java/com/android/internal/widget/DecorCaptionView.java
index e90a8d5..19b68e5 100644
--- a/core/java/com/android/internal/widget/DecorCaptionView.java
+++ b/core/java/com/android/internal/widget/DecorCaptionView.java
@@ -103,6 +103,7 @@
     private final Rect mCloseRect = new Rect();
     private final Rect mMaximizeRect = new Rect();
     private View mClickTarget;
+    private int mRootScrollY;
 
     public DecorCaptionView(Context context) {
         super(context);
@@ -154,10 +155,11 @@
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             final int x = (int) ev.getX();
             final int y = (int) ev.getY();
-            if (mMaximizeRect.contains(x, y)) {
+            // Only offset y for containment tests because the actual views are already translated.
+            if (mMaximizeRect.contains(x, y - mRootScrollY)) {
                 mClickTarget = mMaximize;
             }
-            if (mCloseRect.contains(x, y)) {
+            if (mCloseRect.contains(x, y - mRootScrollY)) {
                 mClickTarget = mClose;
             }
         }
@@ -417,4 +419,16 @@
     public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
         return false;
     }
+
+    /**
+     * Called when {@link android.view.ViewRootImpl} scrolls for adjustPan.
+     */
+    public void onRootViewScrollYChanged(int scrollY) {
+        // Offset the caption opposite the root scroll. This keeps the caption at the
+        // top of the window during adjustPan.
+        if (mCaption != null) {
+            mRootScrollY = scrollY;
+            mCaption.setTranslationY(scrollY);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/TaskPositioner.java b/services/core/java/com/android/server/wm/TaskPositioner.java
index b88e581..4a0831e 100644
--- a/services/core/java/com/android/server/wm/TaskPositioner.java
+++ b/services/core/java/com/android/server/wm/TaskPositioner.java
@@ -43,16 +43,15 @@
 import android.view.BatchedInputEventReceiver;
 import android.view.Choreographer;
 import android.view.Display;
+import android.view.InputApplicationHandle;
 import android.view.InputChannel;
 import android.view.InputDevice;
 import android.view.InputEvent;
+import android.view.InputWindowHandle;
 import android.view.MotionEvent;
 import android.view.WindowManager;
 
 import com.android.internal.annotations.VisibleForTesting;
-import android.view.InputApplicationHandle;
-import android.view.InputWindowHandle;
-import com.android.server.wm.WindowManagerService.H;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -356,12 +355,10 @@
                     + startY + "}");
         }
         mTask = win.getTask();
-        // Use the dim bounds, not the original task bounds. The cursor
-        // movement should be calculated relative to the visible bounds.
-        // Also, use the dim bounds of the task which accounts for
+        // Use the bounds of the task which accounts for
         // multiple app windows. Don't use any bounds from win itself as it
         // may not be the same size as the task.
-        mTask.getDimBounds(mTmpRect);
+        mTask.getBounds(mTmpRect);
         startDrag(resize, preserveOrientation, startX, startY, mTmpRect);
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index b7925f20..d5a6f00 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -842,6 +842,9 @@
         // The offset from the layout containing frame to the actual containing frame.
         final int layoutXDiff;
         final int layoutYDiff;
+        final WindowState imeWin = mWmService.mRoot.getCurrentInputMethodWindow();
+        final boolean isImeTarget =
+                imeWin != null && imeWin.isVisibleNow() && isInputMethodTarget();
         if (inFullscreenContainer || layoutInParentFrame()) {
             // We use the parent frame as the containing frame for fullscreen and child windows
             mWindowFrames.mContainingFrame.set(mWindowFrames.mParentFrame);
@@ -861,15 +864,19 @@
                 mWindowFrames.mContainingFrame.bottom =
                         mWindowFrames.mContainingFrame.top + frozen.height();
             }
-            final WindowState imeWin = mWmService.mRoot.getCurrentInputMethodWindow();
             // IME is up and obscuring this window. Adjust the window position so it is visible.
-            if (imeWin != null && imeWin.isVisibleNow() && isInputMethodTarget()) {
-                if (inFreeformWindowingMode() && mWindowFrames.mContainingFrame.bottom
-                        > mWindowFrames.mContentFrame.bottom) {
-                    // In freeform we want to move the top up directly.
-                    // TODO: Investigate why this is contentFrame not parentFrame.
-                    mWindowFrames.mContainingFrame.top -= mWindowFrames.mContainingFrame.bottom
-                            - mWindowFrames.mContentFrame.bottom;
+            if (isImeTarget) {
+                if (inFreeformWindowingMode()) {
+                    // Push the freeform window up to make room for the IME. However, don't push
+                    // it up past the top of the screen.
+                    final int bottomOverlap = mWindowFrames.mContainingFrame.bottom
+                            - mWindowFrames.mVisibleFrame.bottom;
+                    if (bottomOverlap > 0) {
+                        final int distanceToTop = Math.max(mWindowFrames.mContainingFrame.top
+                                - mWindowFrames.mDisplayFrame.top, 0);
+                        int offs = Math.min(bottomOverlap, distanceToTop);
+                        mWindowFrames.mContainingFrame.top -= offs;
+                    }
                 } else if (!inPinnedWindowingMode() && mWindowFrames.mContainingFrame.bottom
                         > mWindowFrames.mParentFrame.bottom) {
                     // But in docked we want to behave like fullscreen and behave as if the task
@@ -955,9 +962,21 @@
             final int left = Math.max(limitFrame.left + minVisibleWidth - width,
                     Math.min( mWindowFrames.mFrame.left, limitFrame.right - minVisibleWidth));
             mWindowFrames.mFrame.set(left, top, left + width, top + height);
+            final int visBottom = mWindowFrames.mVisibleFrame.bottom;
+            final int contentBottom = mWindowFrames.mContentFrame.bottom;
             mWindowFrames.mContentFrame.set( mWindowFrames.mFrame);
             mWindowFrames.mVisibleFrame.set(mWindowFrames.mContentFrame);
             mWindowFrames.mStableFrame.set(mWindowFrames.mContentFrame);
+            if (isImeTarget && inFreeformWindowingMode()) {
+                // After displacing a freeform window to make room for the ime, any part of
+                // the window still covered by IME should be inset.
+                if (contentBottom + layoutYDiff < mWindowFrames.mContentFrame.bottom) {
+                    mWindowFrames.mContentFrame.bottom = contentBottom + layoutYDiff;
+                }
+                if (visBottom + layoutYDiff < mWindowFrames.mVisibleFrame.bottom) {
+                    mWindowFrames.mVisibleFrame.bottom = visBottom + layoutYDiff;
+                }
+            }
         } else if (mAttrs.type == TYPE_DOCK_DIVIDER) {
             dc.getDockedDividerController().positionDockedStackedDivider(mWindowFrames.mFrame);
             mWindowFrames.mContentFrame.set(mWindowFrames.mFrame);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
index 61c1d39..2ccdb9e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
 import static android.view.DisplayCutout.fromBoundingRect;
@@ -41,6 +42,7 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Mockito;
 
 /**
  * Tests for the {@link WindowState#computeFrameLw} method and other window frame machinery.
@@ -78,17 +80,22 @@
     }
 
     private static class TaskWithBounds extends Task {
-        final Rect mBounds;
+        Rect mBounds;
         final Rect mOverrideDisplayedBounds = new Rect();
         boolean mFullscreenForTest = true;
 
         TaskWithBounds(TaskStack stack, WindowManagerService wm, Rect bounds) {
             super(0, stack, 0, wm, 0, false, new TaskDescription(), null);
-            mBounds = bounds;
             setBounds(bounds);
         }
 
         @Override
+        public int setBounds(Rect bounds) {
+            mBounds = bounds;
+            return super.setBounds(bounds);
+        }
+
+        @Override
         public Rect getBounds() {
             return mBounds;
         }
@@ -513,6 +520,66 @@
         assertEquals(w.getWmDisplayCutout().getDisplayCutout().getSafeInsetRight(), 0);
     }
 
+    @Test
+    public void testFreeformContentInsets() {
+        // fullscreen task doesn't use bounds for computeFrame
+        final Task task = new TaskWithBounds(mStubStack, mWm, null);
+        WindowState w = createWindow(task, MATCH_PARENT, MATCH_PARENT);
+        w.mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
+        task.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        ((TaskWithBounds) task).mFullscreenForTest = false;
+        w.setWindowingMode(WINDOWING_MODE_FREEFORM);
+
+        DisplayContent dc = mWm.getDefaultDisplayContentLocked();
+        dc.mInputMethodTarget = w;
+        WindowState mockIme = mock(WindowState.class);
+        Mockito.doReturn(true).when(mockIme).isVisibleNow();
+        dc.mInputMethodWindow = mockIme;
+
+        // With no insets or system decor all the frames incoming from PhoneWindowManager
+        // are identical.
+        final Rect pf = new Rect(0, 0, 1000, 800);
+        final Rect df = pf;
+        final Rect of = df;
+        final Rect cf = new Rect(pf);
+        cf.bottom -= 400;
+        final Rect vf = new Rect(cf);
+        final Rect sf = new Rect(pf);
+        final Rect dcf = pf;
+
+        // First check that it only gets moved up enough to show window.
+        final Rect winRect = new Rect(200, 200, 300, 500);
+
+        task.setBounds(winRect);
+        w.setBounds(winRect);
+        w.getWindowFrames().setFrames(pf, df, of, cf, vf, dcf, sf, mEmptyRect);
+        w.computeFrameLw();
+
+        final Rect expected = new Rect(winRect.left, cf.bottom - winRect.height(),
+                winRect.right, cf.bottom);
+        assertEquals(expected, w.getFrameLw());
+        assertEquals(expected, w.getContentFrameLw());
+        assertEquals(expected, w.getVisibleFrameLw());
+
+        // Now check that it won't get moved beyond the top and then has appropriate insets
+        winRect.bottom = 600;
+        task.setBounds(winRect);
+        w.setBounds(winRect);
+        w.getWindowFrames().setFrames(pf, df, of, cf, vf, dcf, sf, mEmptyRect);
+        w.computeFrameLw();
+
+        assertFrame(w, winRect.left, 0, winRect.right, winRect.height());
+        expected.top = 0;
+        expected.bottom = cf.bottom;
+        assertContentFrame(w, expected);
+        assertVisibleFrame(w, expected);
+
+        // Check that it's moved back without ime insets
+        w.getWindowFrames().setFrames(pf, df, of, pf, pf, dcf, sf, mEmptyRect);
+        w.computeFrameLw();
+        assertEquals(winRect, w.getFrameLw());
+    }
+
     private WindowStateWithTask createWindow(Task task, int width, int height) {
         final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_APPLICATION);
         attrs.width = width;