Tests for misc keyboard focus navigation fixed

Bug: 62911028
Bug: 62943663
Test: TabHostTest#testKeyboardNavigation, tweaked
      FocusFinderTest#testFindNextFocus to check touching case.

Change-Id: I625029502ae50edbdebb92a112569f49dee0be3f
diff --git a/tests/tests/view/res/layout/focus_finder_layout.xml b/tests/tests/view/res/layout/focus_finder_layout.xml
index 1dea684..8f4dffb 100644
--- a/tests/tests/view/res/layout/focus_finder_layout.xml
+++ b/tests/tests/view/res/layout/focus_finder_layout.xml
@@ -22,7 +22,8 @@
             android:layout_alignParentTop="true">
         <TableRow>
             <android.view.cts.TestButton android:id="@+id/top_left_button"
-                    android:layout_width="60dp"
+                    android:layout_width="20dp"
+                    android:layout_marginRight="40dp"
                     android:layout_height="match_parent"
                     android:text="TL" />
             <android.view.cts.TestButton android:id="@+id/top_right_button"
diff --git a/tests/tests/view/src/android/view/cts/FocusFinderTest.java b/tests/tests/view/src/android/view/cts/FocusFinderTest.java
index afda5aa..0f2119e 100644
--- a/tests/tests/view/src/android/view/cts/FocusFinderTest.java
+++ b/tests/tests/view/src/android/view/cts/FocusFinderTest.java
@@ -94,6 +94,10 @@
         verifyNextFocus(null, View.FOCUS_LEFT, mBottomRight);
         verifyNextFocus(null, View.FOCUS_UP, mBottomRight);
 
+        // Check that left/right traversal works when top/bottom borders are equal.
+        verifyNextFocus(mTopRight, View.FOCUS_LEFT, mTopLeft);
+        verifyNextFocus(mBottomLeft, View.FOCUS_RIGHT, mBottomRight);
+
         // Edge-case where root has focus
         mActivityRule.runOnUiThread(() -> {
             mLayout.setFocusableInTouchMode(true);
diff --git a/tests/tests/widget/res/layout/tabhost_focus.xml b/tests/tests/widget/res/layout/tabhost_focus.xml
new file mode 100644
index 0000000..1229454
--- /dev/null
+++ b/tests/tests/widget/res/layout/tabhost_focus.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+
+    <Button
+        android:id="@+id/before_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Before Button"/>
+
+    <TabHost
+        android:id="@android:id/tabhost"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+
+            <TabWidget
+                android:id="@android:id/tabs"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"></TabWidget>
+
+            <FrameLayout
+                android:id="@android:id/tabcontent"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent">
+
+                <LinearLayout
+                    android:id="@+id/tab1"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:orientation="vertical">
+
+                    <Button
+                        android:id="@+id/tab1_button"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="Tab 1 Content" />
+
+                </LinearLayout>
+
+                <LinearLayout
+                    android:id="@+id/tab2"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:orientation="vertical">
+
+                    <Button
+                        android:id="@+id/tab2_button"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="Tab 2 Content" />
+                </LinearLayout>
+            </FrameLayout>
+        </LinearLayout>
+    </TabHost>
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/tests/widget/src/android/widget/cts/TabHostTest.java b/tests/tests/widget/src/android/widget/cts/TabHostTest.java
index 765fd37..203657d 100644
--- a/tests/tests/widget/src/android/widget/cts/TabHostTest.java
+++ b/tests/tests/widget/src/android/widget/cts/TabHostTest.java
@@ -35,6 +35,7 @@
 import android.support.test.filters.SmallTest;
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
+import android.view.KeyEvent;
 import android.view.View;
 import android.widget.ListView;
 import android.widget.TabHost;
@@ -395,6 +396,48 @@
         assertEquals(TAG_TAB2, tabHost.getCurrentTabTag());
     }
 
+    @UiThreadTest
+    @Test
+    public void testKeyboardNavigation() throws Throwable {
+        mActivity.setContentView(R.layout.tabhost_focus);
+        TabHost tabHost = (TabHost) mActivity.findViewById(android.R.id.tabhost);
+        tabHost.setup();
+        TabSpec spec = tabHost.newTabSpec("Tab 1");
+        spec.setContent(R.id.tab1);
+        spec.setIndicator("Tab 1");
+        tabHost.addTab(spec);
+        spec = tabHost.newTabSpec("Tab 2");
+        spec.setContent(R.id.tab2);
+        spec.setIndicator("Tab 2");
+        tabHost.addTab(spec);
+        View topBut = mActivity.findViewById(R.id.before_button);
+        topBut.requestFocus();
+        assertTrue(topBut.isFocused());
+        mInstrumentation.waitForIdleSync();
+        mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
+        View tabs = mActivity.findViewById(android.R.id.tabs);
+        assertTrue(tabs.hasFocus());
+        View firstTab = tabs.findFocus();
+        mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
+        assertTrue(tabs.hasFocus());
+        int[] shiftKey = new int[]{KeyEvent.KEYCODE_SHIFT_LEFT};
+        sendKeyComboSync(KeyEvent.KEYCODE_TAB, shiftKey);
+        assertTrue(tabs.hasFocus());
+        mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
+        assertTrue(tabs.hasFocus());
+
+        // non-navigation sends focus to content
+        mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_E);
+        assertTrue(mActivity.findViewById(R.id.tab1_button).isFocused());
+        sendKeyComboSync(KeyEvent.KEYCODE_TAB, shiftKey);
+        assertTrue(tabs.hasFocus());
+
+        mActivityRule.runOnUiThread(() -> firstTab.requestFocus());
+        mInstrumentation.waitForIdleSync();
+        sendKeyComboSync(KeyEvent.KEYCODE_TAB, shiftKey);
+        assertTrue(topBut.isFocused());
+    }
+
     private class MyTabContentFactoryText implements TabHost.TabContentFactory {
         public View createTabContent(String tag) {
             final TextView tv = new TextView(mActivity);
@@ -409,4 +452,48 @@
             return lv;
         }
     }
+
+    private static int metaFromKey(int keyCode) {
+        switch(keyCode) {
+            case KeyEvent.KEYCODE_ALT_LEFT: return KeyEvent.META_ALT_LEFT_ON;
+            case KeyEvent.KEYCODE_ALT_RIGHT: return KeyEvent.META_ALT_RIGHT_ON;
+            case KeyEvent.KEYCODE_SHIFT_LEFT: return KeyEvent.META_SHIFT_LEFT_ON;
+            case KeyEvent.KEYCODE_SHIFT_RIGHT: return KeyEvent.META_SHIFT_RIGHT_ON;
+            case KeyEvent.KEYCODE_CTRL_LEFT: return KeyEvent.META_CTRL_LEFT_ON;
+            case KeyEvent.KEYCODE_CTRL_RIGHT: return KeyEvent.META_CTRL_RIGHT_ON;
+            case KeyEvent.KEYCODE_META_LEFT: return KeyEvent.META_META_LEFT_ON;
+            case KeyEvent.KEYCODE_META_RIGHT: return KeyEvent.META_META_RIGHT_ON;
+        }
+        return 0;
+    }
+
+    /**
+     * High-level method for sending a chorded key-combo (modifiers + key). This will send all the
+     * down and up key events as a user would press them (ie. all the modifiers get their own
+     * down and up events).
+     *
+     * @param keyCode The keycode to send while all meta keys are pressed.
+     * @param metaKeys An array of meta key *keycodes* (not modifiers).
+     */
+    private void sendKeyComboSync(int keyCode, int[] metaKeys) {
+        int metaState = 0;
+        if (metaKeys != null) {
+            for (int mk = 0; mk < metaKeys.length; ++mk) {
+                metaState |= metaFromKey(metaKeys[mk]);
+                mInstrumentation.sendKeySync(new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, metaKeys[mk],
+                        0, KeyEvent.normalizeMetaState(metaState)));
+            }
+        }
+        mInstrumentation.sendKeySync(new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, keyCode, 0,
+                KeyEvent.normalizeMetaState(metaState)));
+        mInstrumentation.sendKeySync(new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0,
+                KeyEvent.normalizeMetaState(metaState)));
+        if (metaKeys != null) {
+            for (int mk = 0; mk < metaKeys.length; ++mk) {
+                metaState &= ~metaFromKey(metaKeys[mk]);
+                mInstrumentation.sendKeySync(new KeyEvent(0, 0, KeyEvent.ACTION_UP, metaKeys[mk], 0,
+                        KeyEvent.normalizeMetaState(metaState)));
+            }
+        }
+    }
 }