Adds support for page up, page down, move home, and move end key events.

Bug: 205800548
Test: Added additional unit tests.
Change-Id: I72cbba7bf0a0a3763755e3c23f4121ec9577e6ab
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 05a23875..70782d1 100644
--- a/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
+++ b/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
@@ -115,6 +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;
 
     static {
         final Package pkg = CoordinatorLayout.class.getPackage();
@@ -181,6 +183,13 @@
     // This only exist to prevent GC and object instantiation costs that are present before API 21.
     private final int[] mNestedScrollingV2ConsumedCompat = new int[2];
 
+    // Array to be mutated by calls to nested scrolling related methods triggered by key events.
+    // Because these scrolling events rely on lower level methods using mBehaviorConsumed, we need
+    // a separate variable to save memory. As with the above, this only exist to prevent GC and
+    // object instantiation costs that are
+    // present before API 21.
+    private final int[] mKeyTriggeredScrollConsumed = new int[2];
+
     private boolean mDisallowInterceptReset;
 
     private boolean mIsAttachedToWindow;
@@ -1945,47 +1954,46 @@
             if (event.getAction() == KeyEvent.ACTION_DOWN) {
                 switch (event.getKeyCode()) {
                     case KeyEvent.KEYCODE_DPAD_UP:
-                    case KeyEvent.KEYCODE_DPAD_DOWN:
-                    case KeyEvent.KEYCODE_SPACE:
-
-                        int yScrollDelta;
-
-                        if (event.getKeyCode() == KeyEvent.KEYCODE_SPACE) {
-                            if (event.isShiftPressed()) {
-                                // Places the CoordinatorLayout at the top of the available
-                                // content.
-                                // Note: The delta may represent a value that would overshoot the
-                                // top of the screen, but the children only use as much of the
-                                // delta as they can support, so it will always go exactly to the
-                                // top.
-                                yScrollDelta = -getFullContentHeight();
-                            } else {
-                                // Places the CoordinatorLayout at the bottom of the available
-                                // content.
-                                yScrollDelta = getFullContentHeight() - getHeight();
-                            }
-
-                        } else if (event.isAltPressed()) { // For UP and DOWN KeyEvents
-                            // Full page scroll
-                            yScrollDelta = getHeight();
-
+                        if (event.isAltPressed()) {
+                            // Inverse to move up the screen
+                            handled = moveVertically(-pageDelta());
                         } else {
-                            // Regular arrow scroll
-                            yScrollDelta = (int) (getHeight() * 0.1f);
+                            // Inverse to move up the screen
+                            handled = moveVertically(-lineDelta());
                         }
+                        break;
 
-                        View focusedView = findDeepestFocusedChild(this);
-
-                        // Convert delta to negative if the key event is UP.
-                        if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) {
-                            yScrollDelta = -yScrollDelta;
+                    case KeyEvent.KEYCODE_DPAD_DOWN:
+                        if (event.isAltPressed()) {
+                            handled = moveVertically(pageDelta());
+                        } else {
+                            handled = moveVertically(lineDelta());
                         }
+                        break;
 
-                        handled = manuallyTriggersNestedScrollFromKeyEvent(
-                                focusedView,
-                                yScrollDelta
-                        );
+                    case KeyEvent.KEYCODE_PAGE_UP:
+                        // Inverse to move up the screen
+                        handled = moveVertically(-pageDelta());
+                        break;
 
+                    case KeyEvent.KEYCODE_PAGE_DOWN:
+                        handled = moveVertically(pageDelta());
+                        break;
+
+                    case KeyEvent.KEYCODE_SPACE:
+                        if (event.isShiftPressed()) {
+                            handled = moveVertically(distanceToTop());
+                        } else {
+                            handled = moveVertically(distanceToBottom());
+                        }
+                        break;
+
+                    case KeyEvent.KEYCODE_MOVE_HOME:
+                        handled = moveVertically(distanceToTop());
+                        break;
+
+                    case KeyEvent.KEYCODE_MOVE_END:
+                        handled = moveVertically(distanceToBottom());
                         break;
                 }
             }
@@ -1994,6 +2002,36 @@
         return handled;
     }
 
+    // Distance for moving one arrow key tap.
+    private int lineDelta() {
+        return (int) (getHeight() * KEY_SCROLL_FRACTION_AMOUNT);
+    }
+
+    private int pageDelta() {
+        return getHeight();
+    }
+
+    private int distanceToTop() {
+        // Note: The delta may represent a value that would overshoot the
+        // top of the screen, but the children only use as much of the
+        // delta as they can support, so it will always go exactly to the
+        // top.
+        return -getFullContentHeight();
+    }
+
+    private int distanceToBottom() {
+        return getFullContentHeight() - getHeight();
+    }
+
+    private boolean moveVertically(int yScrollDelta) {
+        View focusedView = findDeepestFocusedChild(this);
+
+        return manuallyTriggersNestedScrollFromKeyEvent(
+                focusedView,
+                yScrollDelta
+        );
+    }
+
     private View findDeepestFocusedChild(View startingParentView) {
         View focusedView = startingParentView;
         while (focusedView != null) {
@@ -2050,6 +2088,10 @@
                 ViewCompat.TYPE_NON_TOUCH
         );
 
+        // Reset consumed values to zero.
+        mKeyTriggeredScrollConsumed[0] = 0;
+        mKeyTriggeredScrollConsumed[1] = 0;
+
         onNestedScroll(
                 focusedView,
                 0,
@@ -2057,12 +2099,12 @@
                 0,
                 yScrollDelta,
                 ViewCompat.TYPE_NON_TOUCH,
-                mBehaviorConsumed
+                mKeyTriggeredScrollConsumed
         );
 
         onStopNestedScroll(focusedView, ViewCompat.TYPE_NON_TOUCH);
 
-        if (mBehaviorConsumed[1] > 0) {
+        if (mKeyTriggeredScrollConsumed[1] > 0) {
             handled = true;
         }
 
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java
index 74a1306..2885cc3 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java
@@ -28,6 +28,7 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
@@ -277,7 +278,229 @@
         // Assert
         // Should trigger a scroll event in parent. Note: OnStartNestedScroll is triggered on
         // key action down only, not key action up, so that is why the count is one.
-        // Should trigger in parent of scroll event.
+        assertEquals(1, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardPageDownInChild_calledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_PAGE_DOWN,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_PAGE_DOWN,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger a scroll event in parent. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        assertEquals(1, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardPageUpInChild_calledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Move to bottom of the child NestedScrollView, so we can scroll up and not go past child.
+        int scrollRange = mChildNestedScrollView.getScrollRange();
+        mChildNestedScrollView.scrollTo(0, scrollRange);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_PAGE_UP,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_PAGE_UP,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger a scroll event in parent. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        assertEquals(1, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardMoveEndInChild_calledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_MOVE_END,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_MOVE_END,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger a scroll event in parent. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        assertEquals(1, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardMoveHomeInChild_calledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Move to bottom of the child NestedScrollView, so we can scroll up and not go past child.
+        int scrollRange = mChildNestedScrollView.getScrollRange();
+        mChildNestedScrollView.scrollTo(0, scrollRange);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_MOVE_HOME,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_MOVE_HOME,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger a scroll event in parent. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        assertEquals(1, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardSpaceBarInChild_calledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_SPACE,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_SPACE,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger a scroll event in parent. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        assertEquals(1, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardShiftSpaceBarInChild_calledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Move to bottom of the child NestedScrollView, so we can scroll up and not go past child.
+        int scrollRange = mChildNestedScrollView.getScrollRange();
+        mChildNestedScrollView.scrollTo(0, scrollRange);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_SPACE,
+                0,
+                KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON
+        );
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_SPACE,
+                0,
+                KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON
+        );
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger a scroll event in parent. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
         assertEquals(1, mParentNestedScrollView.getOnStartNestedScrollCount());
         // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
         assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
@@ -321,8 +544,6 @@
         mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
 
         // Assert
-        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
-        // key action down only, not key action up, so that is why the count is one.
         assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
         // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
         assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
@@ -358,9 +579,6 @@
         mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
 
         // Assert
-        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
-        // key action down only, not key action up, so that is why the count is one.
-        // Should trigger in parent of scroll event.
         assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
         // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
         assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
@@ -397,9 +615,6 @@
         mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
 
         // Assert
-        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
-        // key action down only, not key action up, so that is why the count is one.
-        // Should trigger in parent of scroll event.
         assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
         // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
         assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
@@ -438,9 +653,77 @@
         mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
 
         // Assert
-        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
-        // key action down only, not key action up, so that is why the count is one.
-        // Should trigger in parent of scroll event.
+        assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardPageUpInChildPastTop_notCalledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressPageDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_PAGE_UP,
+                0
+        );
+        mChildNestedScrollView.executeKeyEvent(keyEventPressPageDown);
+
+        KeyEvent keyEventPressPageUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_PAGE_UP,
+                0
+        );
+        mChildNestedScrollView.executeKeyEvent(keyEventPressPageUp);
+
+        // Assert
+        assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardPageDownInChildPastBottom_notCalledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+        // Move to bottom of the child NestedScrollView, so we can try scrolling past it.
+        int scrollRange = mChildNestedScrollView.getScrollRange();
+        mChildNestedScrollView.scrollTo(0, scrollRange);
+
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_PAGE_DOWN,
+                0
+        );
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_PAGE_DOWN,
+                0
+        );
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
         assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
         // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
         assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
@@ -475,9 +758,6 @@
         mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
 
         // Assert
-        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
-        // key action down only, not key action up, so that is why the count is one.
-        // Should trigger in parent of scroll event.
         assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
         // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
         assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
@@ -513,9 +793,6 @@
         mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
 
         // Assert
-        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
-        // key action down only, not key action up, so that is why the count is one.
-        // Should trigger in parent of scroll event.
         assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
         // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
         assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
@@ -701,7 +978,12 @@
         }
 
         @Override
-        public boolean onStartNestedScroll(View child, View target, int axes, int type) {
+        public boolean onStartNestedScroll(
+                @NonNull View child,
+                @NonNull View target,
+                int axes,
+                int type
+        ) {
             mOnStartNestedScrollCount++;
             return super.onStartNestedScroll(child, target, axes, type);
         }
diff --git a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
index dd19e2d..2b0cdce 100644
--- a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -704,22 +704,34 @@
         if (event.getAction() == KeyEvent.ACTION_DOWN) {
             switch (event.getKeyCode()) {
                 case KeyEvent.KEYCODE_DPAD_UP:
-                    if (!event.isAltPressed()) {
-                        handled = arrowScroll(View.FOCUS_UP);
-                    } else {
+                    if (event.isAltPressed()) {
                         handled = fullScroll(View.FOCUS_UP);
+                    } else {
+                        handled = arrowScroll(View.FOCUS_UP);
                     }
                     break;
                 case KeyEvent.KEYCODE_DPAD_DOWN:
-                    if (!event.isAltPressed()) {
-                        handled = arrowScroll(View.FOCUS_DOWN);
-                    } else {
+                    if (event.isAltPressed()) {
                         handled = fullScroll(View.FOCUS_DOWN);
+                    } else {
+                        handled = arrowScroll(View.FOCUS_DOWN);
                     }
                     break;
+                case KeyEvent.KEYCODE_PAGE_UP:
+                    handled = fullScroll(View.FOCUS_UP);
+                    break;
+                case KeyEvent.KEYCODE_PAGE_DOWN:
+                    handled = fullScroll(View.FOCUS_DOWN);
+                    break;
                 case KeyEvent.KEYCODE_SPACE:
                     pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
                     break;
+                case KeyEvent.KEYCODE_MOVE_HOME:
+                    pageScroll(View.FOCUS_UP);
+                    break;
+                case KeyEvent.KEYCODE_MOVE_END:
+                    pageScroll(View.FOCUS_DOWN);
+                    break;
             }
         }