Keep RemoteInputView visible when focused

Scrolls the NotificationStackScrollLayout in response to movement
to ensure that the focused view stays visible. Also makes sure that
the action list is always aligned at the bottom of the notification
to avoid jank during this scrolling when the height changes.

Fixes: 28193862
Fixes: 26919632
Change-Id: I911a873367fe26eafd9fae4bca4e693d0827eba7
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
index 34c9c0d..078b3c6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
@@ -374,10 +374,42 @@
         mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());;
         mUnrestrictedContentHeight = Math.max(contentHeight, getMinHeight());
         selectLayout(mAnimate /* animate */, false /* force */);
+
+        int minHeightHint = getMinContentHeightHint();
+
+        NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
+        if (wrapper != null) {
+            wrapper.setContentHeight(mContentHeight, minHeightHint);
+        }
+
+        wrapper = getVisibleWrapper(mTransformationStartVisibleType);
+        if (wrapper != null) {
+            wrapper.setContentHeight(mContentHeight, minHeightHint);
+        }
+
         updateClipping();
         invalidateOutline();
     }
 
+    /**
+     * @return the minimum apparent height that the wrapper should allow for the purpose
+     *         of aligning elements at the bottom edge. If this is larger than the content
+     *         height, the notification is clipped instead of being further shrunk.
+     */
+    private int getMinContentHeightHint() {
+        if (mIsChildInGroup && (mVisibleType == VISIBLE_TYPE_SINGLELINE
+                || mTransformationStartVisibleType == VISIBLE_TYPE_SINGLELINE)) {
+            return mContext.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.notification_action_list_height);
+        }
+        if (mHeadsUpChild != null) {
+            return mHeadsUpChild.getHeight();
+        } else {
+            return mContractedChild.getHeight() + mContext.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.notification_action_list_height);
+        }
+    }
+
     private void updateContentTransformation() {
         int visibleType = calculateVisibleType();
         if (visibleType != mVisibleType) {
@@ -493,11 +525,15 @@
         } else {
             int visibleType = calculateVisibleType();
             if (visibleType != mVisibleType || force) {
-            View visibleView = getViewForVisibleType(visibleType);
-            if (visibleView != null) {
-                visibleView.setVisibility(VISIBLE);
-                transferRemoteInputFocus(visibleType);
-            }
+                View visibleView = getViewForVisibleType(visibleType);
+                if (visibleView != null) {
+                    visibleView.setVisibility(VISIBLE);
+                    transferRemoteInputFocus(visibleType);
+                }
+                NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType);
+                if (visibleWrapper != null) {
+                    visibleWrapper.setContentHeight(mContentHeight, getMinContentHeightHint());
+                }
 
                 if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null)
                         || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActionListTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActionListTransformState.java
new file mode 100644
index 0000000..c0373be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActionListTransformState.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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.systemui.statusbar.notification;
+
+import android.text.Layout;
+import android.text.TextUtils;
+import android.util.Pools;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A transform state of the action list
+*/
+public class ActionListTransformState extends TransformState {
+
+    private static Pools.SimplePool<ActionListTransformState> sInstancePool
+            = new Pools.SimplePool<>(40);
+
+    @Override
+    protected boolean sameAs(TransformState otherState) {
+        return otherState instanceof ActionListTransformState;
+    }
+
+    public static ActionListTransformState obtain() {
+        ActionListTransformState instance = sInstancePool.acquire();
+        if (instance != null) {
+            return instance;
+        }
+        return new ActionListTransformState();
+    }
+
+    @Override
+    protected void resetTransformedView() {
+        // We need to keep the Y transformation, because this is used to keep the action list
+        // aligned at the bottom, unrelated to transforms.
+        float y = getTransformedView().getTranslationY();
+        super.resetTransformedView();
+        getTransformedView().setTranslationY(y);
+    }
+
+    @Override
+    public void recycle() {
+        super.recycle();
+        sInstancePool.release(this);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
index 889bd5c..7ca2df9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
@@ -41,6 +41,10 @@
     private ProgressBar mProgressBar;
     private TextView mTitle;
     private TextView mText;
+    private View mActionsContainer;
+
+    private int mContentHeight;
+    private int mMinHeightHint;
 
     protected NotificationTemplateViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
         super(ctx, view, row);
@@ -123,6 +127,7 @@
             // It's still a viewstub
             mProgressBar = null;
         }
+        mActionsContainer = mView.findViewById(com.android.internal.R.id.actions_container);
     }
 
     @Override
@@ -225,4 +230,21 @@
                 (int) (gSource * (1f - t) + gTarget * t),
                 (int) (bSource * (1f - t) + bTarget * t));
     }
+
+    @Override
+    public void setContentHeight(int contentHeight, int minHeightHint) {
+        super.setContentHeight(contentHeight, minHeightHint);
+
+        mContentHeight = contentHeight;
+        mMinHeightHint = minHeightHint;
+        updateActionOffset();
+    }
+
+    private void updateActionOffset() {
+        if (mActionsContainer != null) {
+            // We should never push the actions higher than they are in the headsup view.
+            int constrainedContentHeight = Math.max(mContentHeight, mMinHeightHint);
+            mActionsContainer.setTranslationY(constrainedContentHeight - mView.getHeight());
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
index 7a0df1f..22519e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
@@ -158,4 +158,7 @@
 
     public void setShowingLegacyBackground(boolean showing) {
     }
+
+    public void setContentHeight(int contentHeight, int minHeightHint) {
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
index 8207215..4e643f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
@@ -374,6 +374,11 @@
             result.initFrom(view);
             return result;
         }
+        if (view.getId() == com.android.internal.R.id.actions_container) {
+            ActionListTransformState result = ActionListTransformState.obtain();
+            result.initFrom(view);
+            return result;
+        }
         if (view instanceof NotificationHeaderView) {
             HeaderTransformState result = HeaderTransformState.obtain();
             result.initFrom(view);
@@ -467,7 +472,7 @@
         resetTransformedView();
     }
 
-    private void resetTransformedView() {
+    protected void resetTransformedView() {
         mTransformedView.setTranslationX(0);
         mTransformedView.setTranslationY(0);
         mTransformedView.setScaleX(1.0f);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index a855aed..095265a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -272,7 +272,7 @@
 
     public boolean requestScrollTo() {
         findScrollContainer();
-        mScrollContainer.scrollTo(mScrollContainerChild);
+        mScrollContainer.lockScrollTo(mScrollContainerChild);
         return true;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 80eec7e..c66cfba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -243,6 +243,7 @@
             = new ViewTreeObserver.OnPreDrawListener() {
         @Override
         public boolean onPreDraw() {
+            updateForcedScroll();
             updateChildren();
             mChildrenUpdateRequested = false;
             getViewTreeObserver().removeOnPreDrawListener(this);
@@ -334,6 +335,7 @@
     private boolean mDrawBackgroundAsSrc;
     private boolean mFadedOut;
     private boolean mGroupExpandedForMeasure;
+    private View mForcedScroll;
     private float mBackgroundFadeAmount = 1.0f;
     private static final Property<NotificationStackScrollLayout, Float> BACKGROUND_FADE =
             new FloatProperty<NotificationStackScrollLayout>("backgroundFade") {
@@ -591,6 +593,23 @@
         clampScrollPosition();
     }
 
+    private void updateForcedScroll() {
+        if (mForcedScroll != null && (!mForcedScroll.hasFocus()
+                || !mForcedScroll.isAttachedToWindow())) {
+            mForcedScroll = null;
+        }
+        if (mForcedScroll != null) {
+            ExpandableView expandableView = (ExpandableView) mForcedScroll;
+            int positionInLinearLayout = getPositionInLinearLayout(expandableView);
+            int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
+
+            targetScroll = Math.max(0, Math.min(targetScroll, getScrollRange()));
+            if (mOwnScrollY < targetScroll || positionInLinearLayout < mOwnScrollY) {
+                mOwnScrollY = targetScroll;
+            }
+        }
+    }
+
     private void requestChildrenUpdate() {
         if (!mChildrenUpdateRequested) {
             getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
@@ -978,11 +997,19 @@
         mScrollingEnabled = enable;
     }
 
+    @Override
+    public void lockScrollTo(View v) {
+        if (mForcedScroll == v) {
+            return;
+        }
+        mForcedScroll = v;
+        scrollTo(v);
+    }
+
+    @Override
     public boolean scrollTo(View v) {
         ExpandableView expandableView = (ExpandableView) v;
-        int positionInLinearLayout = getPositionInLinearLayout(v);
-        int targetScroll = positionInLinearLayout + expandableView.getIntrinsicHeight() +
-                getImeInset() - getHeight() + getTopPadding();
+        int targetScroll = targetScrollForView(expandableView, getPositionInLinearLayout(v));
 
         if (mOwnScrollY < targetScroll) {
             mScroller.startScroll(mScrollX, mOwnScrollY, 0, targetScroll - mOwnScrollY);
@@ -993,6 +1020,15 @@
         return false;
     }
 
+    /**
+     * @return the scroll necessary to make the bottom edge of {@param v} align with the top of
+     *         the IME.
+     */
+    private int targetScrollForView(ExpandableView v, int positionInLinearLayout) {
+        return positionInLinearLayout + v.getIntrinsicHeight() +
+                getImeInset() - getHeight() + getTopPadding();
+    }
+
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
         mBottomInset = insets.getSystemWindowInsetBottom();
@@ -1111,6 +1147,7 @@
         if (ev.getY() < mQsContainer.getBottom()) {
             return false;
         }
+        mForcedScroll = null;
         initVelocityTrackerIfNotExists();
         mVelocityTracker.addMovement(ev);
 
@@ -2787,6 +2824,14 @@
     }
 
     @Override
+    public void clearChildFocus(View child) {
+        super.clearChildFocus(child);
+        if (mForcedScroll == child) {
+            mForcedScroll = null;
+        }
+    }
+
+    @Override
     public void requestDisallowLongPress() {
         removeLongPressCallback();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ScrollContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ScrollContainer.java
index 17b7871..b9d12ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ScrollContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ScrollContainer.java
@@ -36,6 +36,12 @@
     boolean scrollTo(View v);
 
     /**
+     * Like {@link #scrollTo(View)}, but keeps the scroll locked until the user
+     * scrolls, or {@param v} loses focus or is detached.
+     */
+    void lockScrollTo(View v);
+
+    /**
      * Request that the view does not dismiss for the current touch.
      */
     void requestDisallowDismiss();