Create dpad util for TV functional tests

Adds dpad util to inject key events so that it can be used
by system/app helpers

Bug: 31635441
Change-Id: I47a533055927541d0516a7e4a5f6c21046bf2e92
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/base-app-helpers/Android.mk
index 938ee4c..ce06cc5 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/base-app-helpers/Android.mk
@@ -17,7 +17,7 @@
 
 include $(CLEAR_VARS)
 LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := account-util
+LOCAL_STATIC_JAVA_LIBRARIES := account-util dpad-util
 LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
index 7d7b1b4..afe3c04 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
@@ -19,6 +19,7 @@
 import android.app.Instrumentation;
 import android.platform.test.helpers.exceptions.UiTimeoutException;
 import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.platform.test.utils.DPadUtil;
 import android.support.test.launcherhelper.ILeanbackLauncherStrategy;
 import android.support.test.launcherhelper.LauncherStrategyFactory;
 import android.support.test.uiautomator.By;
@@ -53,12 +54,16 @@
         ERROR_FRAGMENT
     }
 
+    protected DPadUtil mDPadUtil;
+    // TODO: Delete DPadHelper once migrated to using DPadUtil
     protected DPadHelper mDPadHelper;
     public ILeanbackLauncherStrategy mLauncherStrategy;
 
 
     public AbstractLeanbackAppHelper(Instrumentation instr) {
         super(instr);
+        mDPadUtil = new DPadUtil(instr);
+        // TODO: Delete DPadHelper once migrated to using DPadUtil
         mDPadHelper = DPadHelper.getInstance(instr);
         mLauncherStrategy = LauncherStrategyFactory.getInstance(
                 mDevice).getLeanbackLauncherStrategy();
@@ -218,10 +223,10 @@
         }
         while (!focus.hasObject(target)) {
             UiObject2 prev = focus;
-            mDPadHelper.pressDPad(direction);
+            mDPadUtil.pressDPad(direction);
             focus = container.findObject(By.focused(true));
             if (focus == null) {
-                mDPadHelper.pressDPad(Direction.reverse(direction));
+                mDPadUtil.pressDPad(Direction.reverse(direction));
                 focus = container.findObject(By.focused(true));
             }
             if (focus.equals(prev)) {
@@ -321,7 +326,7 @@
                 throw new IllegalStateException("Failed to find a card in row content " + title);
             }
         }
-        mDPadHelper.pressDPadCenter();
+        mDPadUtil.pressDPadCenter();
         mDevice.wait(Until.gone(By.res(getPackage(), "title_text").text(title)),
                 SELECT_WAIT_TIME_MS);
     }
diff --git a/libraries/launcher-helper/Android.mk b/libraries/launcher-helper/Android.mk
index 60c499b..bd9d62f 100644
--- a/libraries/launcher-helper/Android.mk
+++ b/libraries/launcher-helper/Android.mk
@@ -19,6 +19,7 @@
 
 LOCAL_MODULE := launcher-helper-lib
 LOCAL_JAVA_LIBRARIES := ub-uiautomator
+LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
 LOCAL_SDK_VERSION := 21
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
index 16c6b3b..59084ae 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
@@ -19,6 +19,7 @@
 import android.graphics.Point;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.platform.test.utils.DPadUtil;
 import android.support.test.uiautomator.*;
 import android.util.Log;
 
@@ -37,6 +38,7 @@
     private static final int NOTIFICATION_WAIT_TIME = 30000;
 
     protected UiDevice mDevice;
+    protected DPadUtil mDPadUtil = new DPadUtil();
 
 
     /**
@@ -62,7 +64,7 @@
     public void open() {
         // if we see main list view, assume at home screen already
         if (!mDevice.hasObject(getWorkspaceSelector())) {
-            mDevice.pressHome();
+            mDPadUtil.pressHome();
             // ensure launcher is shown
             if (!mDevice.wait(Until.hasObject(getWorkspaceSelector()), SHORT_WAIT_TIME)) {
                 // HACK: dump hierarchy to logcat
@@ -205,12 +207,12 @@
             throw new RuntimeException("Could not find keyboard orb.");
         }
         if (orbButton.isFocused()) {
-            mDevice.pressDPadCenter();
+            mDPadUtil.pressDPadCenter();
         } else {
             // Move the focus to keyboard orb by DPad button.
-            mDevice.pressDPadRight();
+            mDPadUtil.pressDPadRight();
             if (orbButton.isFocused()) {
-                mDevice.pressDPadCenter();
+                mDPadUtil.pressDPadCenter();
             }
         }
         mDevice.wait(Until.gone(keyboardOrb), SHORT_WAIT_TIME);
@@ -225,7 +227,7 @@
         SystemClock.sleep(SHORT_WAIT_TIME);
 
         // Note that Enter key is pressed instead of DPad keys to dismiss leanback IME
-        mDevice.pressEnter();
+        mDPadUtil.pressEnter();
         mDevice.wait(Until.gone(searchEditor), SHORT_WAIT_TIME);
     }
 
@@ -239,7 +241,7 @@
     public UiObject2 selectNotificationRow() {
         if (!isNotificationRowSelected()) {
             open();
-            mDevice.pressHome();    // Home key to move to the first card in the Notification row
+            mDPadUtil.pressHome();    // Home key to move to the first card in the Notification row
         }
         return mDevice.wait(Until.findObject(
                 getNotificationRowSelector().hasDescendant(By.focused(true), 3)), SHORT_WAIT_TIME);
@@ -252,7 +254,7 @@
     public UiObject2 selectSearchRow() {
         if (!isSearchRowSelected()) {
             selectNotificationRow();
-            mDevice.pressDPadUp();
+            mDPadUtil.pressDPadUp();
         }
         return mDevice.wait(Until.findObject(
                 getSearchRowSelector().hasDescendant(By.focused(true))), SHORT_WAIT_TIME);
@@ -392,19 +394,19 @@
                 // The sequence of moving should be kept in the following order so as not to
                 // be stuck in case that the apps row are not even.
                 if (dx < -MARGIN) {
-                    mDevice.pressDPadLeft();
+                    mDPadUtil.pressDPadLeft();
                     continue;
                 }
                 if (dy < -MARGIN) {
-                    mDevice.pressDPadUp();
+                    mDPadUtil.pressDPadUp();
                     continue;
                 }
                 if (dx > MARGIN) {
-                    mDevice.pressDPadRight();
+                    mDPadUtil.pressDPadRight();
                     continue;
                 }
                 if (dy > MARGIN) {
-                    mDevice.pressDPadDown();
+                    mDPadUtil.pressDPadDown();
                     continue;
                 }
                 throw new RuntimeException(
@@ -419,7 +421,7 @@
 
         // The app icon is already found and focused.
         long ready = SystemClock.uptimeMillis();
-        mDevice.pressDPadCenter();
+        mDPadUtil.pressDPadCenter();
         if (!mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT)) {
             Log.w(LOG_TAG, "no new window detected after app launch attempt.");
             return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
@@ -467,12 +469,7 @@
                         appName, card.getContentDescription()));
 
         // Click and wait until the Notification card opens
-        return mDevice.performActionAndWait(new Runnable() {
-            @Override
-            public void run() {
-                mDevice.pressDPadCenter();
-            }
-        }, Until.newWindow(), APP_LAUNCH_TIMEOUT);
+        return mDPadUtil.pressDPadCenterAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT);
     }
 
     protected boolean isSearchRowSelected() {
@@ -539,7 +536,7 @@
 
     protected UiObject2 findNotificationCard(BySelector selector) {
         // Move to the first notification, Search to the right
-        mDevice.pressHome();
+        mDPadUtil.pressHome();
 
         // Find if a focused card matches a given selector
         UiObject2 currentFocus = mDevice.findObject(getNotificationRowSelector())
@@ -549,7 +546,7 @@
             if (currentFocus.hasObject(selector)) {
                 return currentFocus;   // Found
             }
-            mDevice.pressDPadRight();
+            mDPadUtil.pressDPadRight();
             previousFocus = currentFocus;
             currentFocus = mDevice.findObject(getNotificationRowSelector())
                     .findObject(By.res(getSupportedLauncherPackage(), "card").focused(true));
@@ -565,7 +562,7 @@
         String prevText = focusedIcon.getContentDescription();
         String nextText;
         do {
-            mDevice.pressDPadLeft();
+            mDPadUtil.pressDPadLeft();
             appIcon = container.findObject(app);
             if (appIcon != null) {
                 return appIcon;
@@ -577,7 +574,7 @@
 
         // If we haven't found it yet, search by going right
         do {
-            mDevice.pressDPadRight();
+            mDPadUtil.pressDPadRight();
             appIcon = container.findObject(app);
             if (appIcon != null) {
                 return appIcon;
@@ -609,11 +606,7 @@
                 return rowObject;   // Found
             }
 
-            if (direction == Direction.DOWN) {
-                mDevice.pressDPadDown();
-            } else if (direction == Direction.UP) {
-                mDevice.pressDPadUp();
-            }
+            mDPadUtil.pressDPad(direction);
             prevFocused = currentFocused;
             currentFocused = mDevice.findObject(By.focused(true));
         }
@@ -641,12 +634,7 @@
         if (button == null) {
             throw new IllegalStateException("Restricted Profile not found on launcher");
         }
-        mDevice.performActionAndWait(new Runnable() {
-            @Override
-            public void run() {
-                mDevice.pressDPadCenter();
-            }
-        }, Until.newWindow(), APP_LAUNCH_TIMEOUT);
+        mDPadUtil.pressDPadCenterAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT);
     }
 
     protected UiObject2 findSettingInRow(BySelector selector, Direction direction) {
@@ -665,11 +653,7 @@
                 return setting;
             }
 
-            if (direction == Direction.RIGHT) {
-                mDevice.pressDPadRight();
-            } else if (direction == Direction.LEFT) {
-                mDevice.pressDPadLeft();
-            }
+            mDPadUtil.pressDPad(direction);
             mDevice.waitForIdle();
             prevFocused = currentFocused;
             currentFocused = mDevice.findObject(By.focused(true));
diff --git a/utils/dpad/Android.mk b/utils/dpad/Android.mk
new file mode 100644
index 0000000..f211bc7
--- /dev/null
+++ b/utils/dpad/Android.mk
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+# -----------------------------------------------------------------------
+# The static library that platform/app helpers can link against
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := dpad-util
+LOCAL_JAVA_LIBRARIES := ub-uiautomator android-support-test
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/utils/dpad/src/android/platform/test/utils/DPadUtil.java b/utils/dpad/src/android/platform/test/utils/DPadUtil.java
new file mode 100644
index 0000000..dfd7fdf
--- /dev/null
+++ b/utils/dpad/src/android/platform/test/utils/DPadUtil.java
@@ -0,0 +1,169 @@
+/*
+ * 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 android.platform.test.utils;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.EventCondition;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.io.IOException;
+
+
+public class DPadUtil {
+
+    private static final String TAG = DPadUtil.class.getSimpleName();
+    private static final long DPAD_DEFAULT_WAIT_TIME_MS = 1000; // 1 sec
+    private UiDevice mDevice;
+
+
+    public DPadUtil() {
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    public DPadUtil(Instrumentation instrumentation) {
+        mDevice = UiDevice.getInstance(instrumentation);
+    }
+
+    public boolean pressDPad(Direction direction) {
+        return pressDPad(direction, 1, DPAD_DEFAULT_WAIT_TIME_MS);
+    }
+
+    public void pressDPad(Direction direction, long repeat) {
+        pressDPad(direction, repeat, DPAD_DEFAULT_WAIT_TIME_MS);
+    }
+
+    /**
+     * Presses DPad button of the same direction for the count times.
+     * It sleeps between each press for DPAD_DEFAULT_WAIT_TIME_MS.
+     *
+     * @param direction the direction of the button to press.
+     * @param repeat the number of times to press the button.
+     * @param timeout the timeout for the wait.
+     * @return true if the last key simulation is successful, else return false
+     */
+    public boolean pressDPad(Direction direction, long repeat, long timeout) {
+        int iteration = 0;
+        boolean result = false;
+        while (iteration++ < repeat) {
+            switch (direction) {
+                case LEFT:
+                    result = mDevice.pressDPadLeft();
+                    break;
+                case RIGHT:
+                    result = mDevice.pressDPadRight();
+                    break;
+                case UP:
+                    result = mDevice.pressDPadUp();
+                    break;
+                case DOWN:
+                    result = mDevice.pressDPadDown();
+                    break;
+            }
+            SystemClock.sleep(timeout);
+        }
+        return result;
+    }
+
+    public boolean pressDPadLeft() {
+        return mDevice.pressDPadLeft();
+    }
+
+    public boolean pressDPadRight() {
+        return mDevice.pressDPadRight();
+    }
+
+    public boolean pressDPadUp() {
+        return mDevice.pressDPadUp();
+    }
+
+    public boolean pressDPadDown() {
+        return mDevice.pressDPadDown();
+    }
+
+    public boolean pressHome() {
+        return mDevice.pressHome();
+    }
+
+    public boolean pressBack() {
+        return mDevice.pressBack();
+    }
+
+    public boolean pressDPadCenter() {
+        return mDevice.pressDPadCenter();
+    }
+
+    public boolean pressEnter() {
+        return mDevice.pressEnter();
+    }
+
+    public boolean pressPipKey() {
+        return mDevice.pressKeyCode(KeyEvent.KEYCODE_WINDOW);
+    }
+
+    public boolean pressKeyCode(int keyCode) {
+        return mDevice.pressKeyCode(keyCode);
+    }
+
+    public boolean longPressKeyCode(int keyCode) {
+        try {
+            mDevice.executeShellCommand(String.format("input keyevent --longpress %d", keyCode));
+            return true;
+        } catch (IOException e) {
+            // Ignore
+            Log.w(TAG, String.format("Failed to long press the key code: %d", keyCode));
+            return false;
+        }
+    }
+
+    /**
+     * Press the key code, and waits for the given condition to become true.
+     * @param condition
+     * @param keyCode
+     * @param timeout
+     * @param <R>
+     * @return
+     */
+    public <R> R pressKeyCodeAndWait(int keyCode, EventCondition<R> condition, long timeout) {
+        return mDevice.performActionAndWait(new KeyEventRunnable(keyCode), condition, timeout);
+    }
+
+    public <R> R pressDPadCenterAndWait(EventCondition<R> condition, long timeout) {
+        return mDevice.performActionAndWait(new KeyEventRunnable(KeyEvent.KEYCODE_DPAD_CENTER),
+                condition, timeout);
+    }
+
+    public <R> R pressEnterAndWait(EventCondition<R> condition, long timeout) {
+        return mDevice.performActionAndWait(new KeyEventRunnable(KeyEvent.KEYCODE_ENTER),
+                condition, timeout);
+    }
+
+    private class KeyEventRunnable implements Runnable {
+        private int mKeyCode;
+        public KeyEventRunnable(int keyCode) {
+            mKeyCode = keyCode;
+        }
+        @Override
+        public void run() {
+            mDevice.pressKeyCode(mKeyCode);
+        }
+    }
+}