Keyboard down properly collapses App Bar with recyclerview in CoordinatorLayout.

Bug: 330062053
Test: Included in CL.
Change-Id: I7eac424ed279da7bfb00885df168de3fdaf750e1
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml b/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml
index 0dedce5..967e398 100644
--- a/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml
@@ -29,6 +29,12 @@
             android:name="androidx.coordinatorlayout.widget.CoordinatorWithNestedScrollViewsActivity"
             />
 
+        <activity
+            android:exported="true"
+            android:theme="@style/Theme.AppCompat.Light"
+            android:name="androidx.coordinatorlayout.widget.CoordinatorWithRecyclerViewActivity"
+            />
+
         <activity android:name="androidx.coordinatorlayout.widget.DynamicCoordinatorLayoutActivity"/>
 
     </application>
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorLayoutWithRecyclerViewKeyEventTest.java b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorLayoutWithRecyclerViewKeyEventTest.java
new file mode 100644
index 0000000..eed663e
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorLayoutWithRecyclerViewKeyEventTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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 androidx.coordinatorlayout.widget;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.pressKey;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Rect;
+import android.view.KeyEvent;
+import android.view.View;
+
+import androidx.coordinatorlayout.test.R;
+import androidx.coordinatorlayout.testutils.AppBarStateChangedListener;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.testutils.PollingCheck;
+
+import com.google.android.material.appbar.AppBarLayout;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+@SuppressWarnings({"unchecked", "rawtypes"})
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class CoordinatorLayoutWithRecyclerViewKeyEventTest {
+
+    @Rule
+    public ActivityScenarioRule<CoordinatorWithRecyclerViewActivity> mActivityScenarioRule =
+            new ActivityScenarioRule(CoordinatorWithRecyclerViewActivity.class);
+
+    private AppBarLayout mAppBarLayout;
+    private RecyclerView mRecyclerView;
+    // Used to verify that the RecyclerView's item location is zero when using the UP Key.
+    private LinearLayoutManager mLinearLayoutManager;
+
+    private AppBarStateChangedListener.State mAppBarState =
+            AppBarStateChangedListener.State.UNKNOWN;
+
+    public static Matcher<View> isAtLeastHalfVisible() {
+        return new TypeSafeMatcher<View>() {
+            @Override
+            protected boolean matchesSafely(View view) {
+                Rect rect = new Rect();
+                return view.getGlobalVisibleRect(rect)
+                        && rect.width() * rect.height() >= (view.getWidth() * view.getHeight()) / 2;
+            }
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("is at least half visible on screen");
+            }
+        };
+    }
+
+    @Before
+    public void setup() {
+        mActivityScenarioRule.getScenario().onActivity(activity -> {
+            mAppBarLayout = activity.mAppBarLayout;
+            mRecyclerView = activity.mRecyclerView;
+            mLinearLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
+
+            mAppBarLayout.addOnOffsetChangedListener(new AppBarStateChangedListener() {
+                @Override
+                public void onStateChanged(AppBarLayout appBarLayout, State state) {
+                    mAppBarState = state;
+                }
+            });
+        });
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+
+    /*** Tests ***/
+    @Test
+    @LargeTest
+    public void isCollapsingToolbarExpanded_swipeDownMultipleKeysUp_isExpanded() {
+        onView(withId(R.id.recycler_view)).check(matches(isAtLeastHalfVisible()));
+
+        // Scrolls down content and collapses the CollapsingToolbarLayout in the AppBarLayout.
+        onView(withId(R.id.coordinator)).perform(swipeUp());
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        // Espresso doesn't properly support swipeUp() with a CoordinatorLayout,
+        // AppBarLayout/CollapsingToolbarLayout, and RecyclerView. From testing, it only
+        // handles waiting until the AppBarLayout/CollapsingToolbarLayout is finished with its
+        // transition, NOT waiting until the RecyclerView is finished with its scrolling.
+        // This PollingCheck waits until the scroll is finished in the RecyclerView.
+        AtomicInteger previousScroll = new AtomicInteger();
+        PollingCheck.waitFor(() -> {
+            AtomicInteger currentScroll = new AtomicInteger();
+
+            mActivityScenarioRule.getScenario().onActivity(activity -> {
+                currentScroll.set(activity.mRecyclerView.getScrollY());
+            });
+
+            boolean isDone = currentScroll.get() == previousScroll.get();
+            previousScroll.set(currentScroll.get());
+
+            return isDone;
+        });
+
+        // Verifies the CollapsingToolbarLayout in the AppBarLayout is collapsed.
+        assertEquals(AppBarStateChangedListener.State.COLLAPSED, mAppBarState);
+        onView(withId(R.id.recycler_view)).check(matches(isCompletelyDisplayed()));
+
+        // First up keystroke gains focus (doesn't move any content).
+        onView(withId(R.id.recycler_view)).perform(pressKey(KeyEvent.KEYCODE_DPAD_UP));
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        // Retrieve top visible item in the RecyclerView.
+        int currentTopVisibleItem = mLinearLayoutManager.findFirstCompletelyVisibleItemPosition();
+
+        // Scroll up to the 0 position in the RecyclerView via UP Keystroke.
+        while (currentTopVisibleItem > 0) {
+            onView(withId(R.id.recycler_view)).perform(pressKey(KeyEvent.KEYCODE_DPAD_UP));
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+            currentTopVisibleItem = mLinearLayoutManager.findFirstCompletelyVisibleItemPosition();
+        }
+
+        // This is a fail-safe in case the DPAD UP isn't making any changes, we break out of the
+        // loop.
+        float previousAppBarLayoutY = 0.0f;
+
+        // Performs a key press until the app bar is either expanded completely or no changes are
+        // made in the app bar between the previous call and the current call (failure case).
+        while (mAppBarState != AppBarStateChangedListener.State.EXPANDED
+                && (mAppBarLayout.getY() != previousAppBarLayoutY)
+        ) {
+            previousAppBarLayoutY = mAppBarLayout.getY();
+
+            // Partially expands the CollapsingToolbarLayout.
+            onView(withId(R.id.recycler_view)).perform(pressKey(KeyEvent.KEYCODE_DPAD_UP));
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        }
+
+        // Checks CollapsingToolbarLayout (in the AppBarLayout) is fully expanded.
+        assertEquals(AppBarStateChangedListener.State.EXPANDED, mAppBarState);
+    }
+
+    @Test
+    @LargeTest
+    public void doesAppBarCollapse_pressKeyboardDownMultipleTimes() {
+        onView(withId(R.id.recycler_view)).check(matches(isAtLeastHalfVisible()));
+
+        // Scrolls down content (key) and collapses the CollapsingToolbarLayout in the AppBarLayout.
+        // Gains focus
+        onView(withId(R.id.recycler_view)).perform(pressKey(KeyEvent.KEYCODE_DPAD_DOWN));
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        // This is a fail-safe in case the DPAD UP isn't making any changes, we break out of the
+        // loop.
+        float previousAppBarLayoutY = 0.0f;
+
+        // Performs a key press until the app bar is either completely collapsed or no changes are
+        // made in the app bar between the previous call and the current call (failure case).
+        while (mAppBarState != AppBarStateChangedListener.State.COLLAPSED
+                && (mAppBarLayout.getY() != previousAppBarLayoutY)
+        ) {
+            previousAppBarLayoutY = mAppBarLayout.getY();
+
+            // Partial collapse of the CollapsingToolbarLayout.
+            onView(withId(R.id.recycler_view)).perform(pressKey(KeyEvent.KEYCODE_DPAD_DOWN));
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        }
+
+        // Espresso doesn't properly support down with a CoordinatorLayout,
+        // AppBarLayout/CollapsingToolbarLayout, and RecyclerView. From testing, it only
+        // handles waiting until the AppBarLayout/CollapsingToolbarLayout is finished with its
+        // transition, NOT waiting until the RecyclerView is finished with its scrolling.
+        // This PollingCheck waits until the scroll is finished in the RecyclerView.
+        AtomicInteger previousScroll = new AtomicInteger();
+        PollingCheck.waitFor(() -> {
+            AtomicInteger currentScroll = new AtomicInteger();
+
+            mActivityScenarioRule.getScenario().onActivity(activity -> {
+                currentScroll.set(activity.mRecyclerView.getScrollY());
+            });
+
+            boolean isDone = currentScroll.get() == previousScroll.get();
+            previousScroll.set(currentScroll.get());
+
+            return isDone;
+        });
+
+        // Verifies the CollapsingToolbarLayout in the AppBarLayout is collapsed.
+        assertEquals(AppBarStateChangedListener.State.COLLAPSED, mAppBarState);
+        onView(withId(R.id.recycler_view)).check(matches(isCompletelyDisplayed()));
+    }
+}
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorWithRecyclerViewActivity.java b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorWithRecyclerViewActivity.java
new file mode 100644
index 0000000..52c3a7a
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorWithRecyclerViewActivity.java
@@ -0,0 +1,114 @@
+/*
+ * 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 androidx.coordinatorlayout.widget;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.coordinatorlayout.BaseTestActivity;
+import androidx.coordinatorlayout.test.R;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.appbar.AppBarLayout;
+import com.google.android.material.appbar.CollapsingToolbarLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CoordinatorWithRecyclerViewActivity extends BaseTestActivity {
+    AppBarLayout mAppBarLayout;
+    RecyclerView mRecyclerView;
+
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.activity_coordinator_with_recycler_view;
+    }
+
+    @Override
+    protected void onContentViewSet() {
+        mAppBarLayout = findViewById(R.id.app_bar_layout);
+        mRecyclerView = findViewById(R.id.recycler_view);
+
+        CollapsingToolbarLayout collapsingToolbarLayout =
+                findViewById(R.id.collapsing_toolbar_layout);
+
+        collapsingToolbarLayout.setTitle("Collapsing Bar Test");
+
+        mRecyclerView = findViewById(R.id.recycler_view);
+        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+
+        List<String> data = new ArrayList<String>();
+        for (int index = 0; index < 14; index++) {
+            data.add(String.valueOf(index));
+        }
+
+        MyAdapter adapter = new MyAdapter(data);
+        mRecyclerView.setAdapter(adapter);
+    }
+
+    public static class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
+        private final List<String> mDataForItems;
+
+        public MyAdapter(@NonNull List<String> items) {
+            this.mDataForItems = items;
+        }
+
+        public static class ViewHolder extends RecyclerView.ViewHolder {
+            @NonNull public TextView textViewHeader;
+            @NonNull public TextView textViewSubHeader;
+
+            public ViewHolder(@NonNull View itemView) {
+                super(itemView);
+                textViewHeader = itemView.findViewById(R.id.textViewHeader);
+                textViewSubHeader = itemView.findViewById(R.id.textViewSubHeader);
+            }
+        }
+
+        @NonNull
+        @Override
+        public MyAdapter.ViewHolder onCreateViewHolder(
+                @NonNull ViewGroup parent,
+                int viewType
+        ) {
+            View itemView = LayoutInflater.from(parent.getContext()).inflate(
+                    R.layout.recycler_view_with_collapsing_toolbar_list_item,
+                    parent,
+                    false
+            );
+            return new ViewHolder(itemView);
+        }
+
+        @Override
+        public void onBindViewHolder(
+                MyAdapter.ViewHolder holder,
+                int position
+        ) {
+            String number = mDataForItems.get(position);
+
+            holder.textViewHeader.setText(number);
+            holder.textViewSubHeader.setText("Sub Header for " + number);
+        }
+
+        @Override
+        public int getItemCount() {
+            return mDataForItems.size();
+        }
+    }
+}
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/activity_coordinator_with_recycler_view.xml b/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/activity_coordinator_with_recycler_view.xml
new file mode 100644
index 0000000..91bd00f
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/activity_coordinator_with_recycler_view.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<!-- Uses a recyclerview with collapsing app bar AND sets scroll flag to include snap. -->
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/coordinator"
+    android:fitsSystemWindows="true"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <!-- App Bar -->
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/app_bar_layout"
+        android:layout_width="match_parent"
+        android:layout_height="200dp">
+
+        <com.google.android.material.appbar.CollapsingToolbarLayout
+            android:id="@+id/collapsing_toolbar_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:expandedTitleMarginStart="48dp"
+            app:expandedTitleMarginEnd="64dp"
+            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
+
+            <View android:layout_width="match_parent"
+                android:layout_height="200dp"
+                android:background="#FF0000"/>
+
+            <androidx.appcompat.widget.Toolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                app:layout_collapseMode="pin" />
+        </com.google.android.material.appbar.CollapsingToolbarLayout>
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <!-- Content -->
+    <FrameLayout
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:orientation="vertical"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+        </androidx.recyclerview.widget.RecyclerView>
+    </FrameLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/recycler_view_with_collapsing_toolbar_list_item.xml b/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/recycler_view_with_collapsing_toolbar_list_item.xml
new file mode 100644
index 0000000..d44cdf1
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/recycler_view_with_collapsing_toolbar_list_item.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:padding="16dp"
+    android:focusable="true">
+    <TextView
+        android:id="@+id/textViewHeader"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="24sp" />
+    <TextView
+        android:id="@+id/textViewSubHeader"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="14sp" />
+</LinearLayout>
diff --git a/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java b/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
index 36c53be..53ed475 100644
--- a/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
+++ b/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
@@ -115,8 +115,8 @@
         NestedScrollingParent3 {
     static final String TAG = "CoordinatorLayout";
     static final String WIDGET_PACKAGE_NAME;
-    // For the UP/DOWN keys, we scroll 1/10th of the screen.
-    private static final float KEY_SCROLL_FRACTION_AMOUNT = 0.1f;
+    // For the UP/DOWN keys, we scroll 20% of the screen.
+    private static final float KEY_SCROLL_FRACTION_AMOUNT = 0.2f;
 
     static {
         final Package pkg = CoordinatorLayout.class.getPackage();
@@ -2087,6 +2087,16 @@
                 ViewCompat.TYPE_NON_TOUCH
         );
 
+        onNestedPreScroll(
+                focusedView,
+                0,
+                yScrollDelta,
+                mKeyTriggeredScrollConsumed,
+                ViewCompat.TYPE_NON_TOUCH
+        );
+
+        int yScrollDeltaConsumed = mKeyTriggeredScrollConsumed[1];
+
         // Reset consumed values to zero.
         mKeyTriggeredScrollConsumed[0] = 0;
         mKeyTriggeredScrollConsumed[1] = 0;
@@ -2094,7 +2104,7 @@
         onNestedScroll(
                 focusedView,
                 0,
-                0,
+                yScrollDeltaConsumed,
                 0,
                 yScrollDelta,
                 ViewCompat.TYPE_NON_TOUCH,