Add install functionality for the Play Store helper.

* #installApp
* #isAppInstalled
* add the dialogs to dismiss
* add #isRootPackage to IStandardAppHelper/AbstractStandardAppHelper
* follow best practices

Bug: 30808687

Change-Id: I8637022ae07dbdfc7040d77a12bed3e1f206a6f3
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java
index 723565a..90f941e 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java
@@ -25,18 +25,32 @@
     }
 
     /**
-     * Setup expectations: The search bar is visible.
+     * Setup expectations: The app is open.
      *
-     * Selects the search bar, enters a query, and displays the results. Blocks until the results
-     * are selectable.
+     * Looks for the search bar or button by scrolling up or pressing back. It then enters a query,
+     * and displays the results. This method blocks until the results are selectable.
      */
     public abstract void doSearch(String query);
 
     /**
      * Setup expectations: There are visible search results.
      *
-     * Opens the necessary categories and enters the first search result. Blocks until the process
-     * is complete.
+     * Selects the first search result card and blocks until the app's install page is open.
      */
     public abstract void selectFirstResult();
+
+    /**
+     * Setup expectations: An app's install page is open, but the app is not installed.
+     *
+     * Press the install button and dismiss any confirmation dialogs. This method will block until
+     * the app starts downloading, though installation cannot be guaranteed.
+     */
+    public abstract void installApp();
+
+    /**
+     * Setup expectations: An app's install page is open.
+     *
+     * @return true, if the app is already installed, or false if not.
+     */
+    public abstract boolean isAppInstalled();
 }
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java
index 93fd1fe..3c967c7 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java
@@ -85,6 +85,14 @@
         return version;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isAppInForeground() {
+        return mDevice.hasObject(By.pkg(getPackage()).depth(0));
+    }
+
     protected int getOrientation() {
         return mInstrumentation.getContext().getResources().getConfiguration().orientation;
     }
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
index 3c5cde1..d372fe4 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
@@ -66,4 +66,11 @@
      * @return the version as a String
      */
     abstract String getVersion() throws NameNotFoundException;
+
+    /**
+     * Setup expectations: None
+     *
+     * @return true, if this app's package is the root (depth 0), and false otherwise
+     */
+    abstract boolean isAppInForeground();
 }
diff --git a/libraries/play-store-app-helper/src/android/platform/test/helpers/PlayStoreHelperImpl.java b/libraries/play-store-app-helper/src/android/platform/test/helpers/PlayStoreHelperImpl.java
index 5705bec..add04b2 100644
--- a/libraries/play-store-app-helper/src/android/platform/test/helpers/PlayStoreHelperImpl.java
+++ b/libraries/play-store-app-helper/src/android/platform/test/helpers/PlayStoreHelperImpl.java
@@ -33,6 +33,10 @@
     private static final String LOG_TAG = PlayStoreHelperImpl.class.getSimpleName();
     private static final String UI_PACKAGE = "com.android.vending";
 
+    private static final long LONG_TOS_DIALOG_WAIT = 20000;
+    private static final long LOAD_RESULT_TRANSITION = 10000;
+    private static final long INSTALL_DELAY = 5000;
+
     public PlayStoreHelperImpl(Instrumentation instr) {
         super(instr);
     }
@@ -58,9 +62,19 @@
      */
     @Override
     public void dismissInitialDialogs() {
-        UiObject2 tos = mDevice.findObject(By.res(UI_PACKAGE, "positive_button"));
-        if (tos != null) {
-            tos.clickAndWait(Until.newWindow(), 5000);
+        if (!isAppInForeground()) {
+            throw new IllegalStateException("The Play Store app is not in the foreground.");
+        }
+        // Dismiss the ToS dialog by pressing accept
+        if (mDevice.wait(Until.hasObject(
+                By.textContains("Google Play Terms of Service")), LONG_TOS_DIALOG_WAIT)) {
+            mDevice.findObject(getPositiveButtonSelector()).click();
+            boolean home = mDevice.wait(Until.hasObject(getSearchBoxSelector()), 10000);
+            if (!home) {
+                throw new UnknownUiException("Failed to reach the home screen.");
+            }
+        } else {
+            throw new UnknownUiException("Unable to find ToS");
         }
     }
 
@@ -69,39 +83,42 @@
      */
     @Override
     public void doSearch(String query) {
-        // Back up and scroll up until search is visible
-        for (int retries = 3; retries > 0; retries--) {
-            if (getSearchBox() != null) {
+        if (!isAppInForeground()) {
+            throw new IllegalStateException("The Play Store app is not in the foreground.");
+        }
+        // Scroll up or press back until searching is available, or fail
+        UiObject2 search = null;
+        for (int retries = 5; retries > 0; retries--) {
+            // Search for the search box
+            search = mDevice.findObject(getSearchBoxSelector());
+            if (search != null) {
                 break;
-            } else {
-                UiObject2 scroller = getScrollContainer();
-                if (scroller != null) {
-                    scroller.scroll(Direction.UP, 100.0f);
-                } else {
-                    mDevice.pressBack();
-                }
+            }
+            // Search for the search button
+            search = mDevice.findObject(getSearchButtonSelector());
+            if (search != null) {
+                break;
+            }
+            // Scroll up or back out
+            if (!scrollPage(Direction.UP, 100.0f)) {
+                mDevice.pressBack();
             }
         }
-
-        //Interact with the search box
-        UiObject2 searchBox = getSearchBox();
-        if (searchBox == null) {
-            throw new UnknownUiException("Unable to select the search box.");
+        if (search == null) {
+            throw new UnknownUiException("Failed to find a search method.");
         }
-        searchBox.click();
+        search.click();
+        // After pressing, the search element becomes the edit text box
         UiObject2 edit = mDevice.wait(
                 Until.findObject(By.clazz(EditText.class)), 5000);
         if (edit == null) {
-            throw new UnknownUiException("Could not find edit text box.");
+            throw new UnknownUiException("Failed to find an edit text.");
         }
         edit.setText(query);
         mDevice.pressEnter();
-
-        // Wait until the search results container is open
-        boolean success = mDevice.wait(Until.hasObject(
-                By.res(UI_PACKAGE, "search_results_list")), 5000);
-        if (!success) {
-            throw new UnknownUiException("Could not find search results");
+        // Validate the end criteria that the search list is visible
+        if (!mDevice.wait(Until.hasObject(getSearchResultSelector()), 5000)) {
+            throw new UnknownUiException("Failed to find the search results.");
         }
     }
 
@@ -110,50 +127,103 @@
      */
     @Override
     public void selectFirstResult() {
-        try {
-            if (getVersion().startsWith("5.")) {
-                expandSection("Apps");
-            }
-        } catch (NameNotFoundException e) {
-            Log.e(LOG_TAG, "Unable to find version for package: " + UI_PACKAGE);
+        if (!mDevice.hasObject(getSearchResultSelector())) {
+            throw new IllegalStateException("No available search result list.");
         }
         UiObject2 result = mDevice.findObject(By.res(UI_PACKAGE, "play_card"));
         if (result == null) {
-            throw new UnknownUiException("Failed to find a result card");
+            throw new UnknownUiException("Failed to find a search result card.");
         }
         result.click();
+        if (!mDevice.wait(Until.hasObject(getAppTitleSelector()), LOAD_RESULT_TRANSITION)) {
+            throw new UnknownUiException("Failed to find the app page open.");
+        }
     }
 
-    private void expandSection(String header) {
-        for (int retries = 3; retries > 0; retries--) {
-            BySelector section = By.res(UI_PACKAGE, "header_title_main").text(header);
-            UiObject2 title = mDevice.findObject(section);
-            if (title != null) {
-                title.click();
-                mDevice.wait(Until.gone(section), 5000);
-                return;
-            } else {
-                UiObject2 container = mDevice.findObject(By.res(UI_PACKAGE, "search_results_list"));
-                container.scroll(Direction.DOWN, 1.0f);
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void installApp() {
+        // #isAppInstalled also verifies that this is an app page
+        if (isAppInstalled()) {
+            throw new IllegalStateException("This app is already installed.");
+        }
+
+        UiObject2 install = mDevice.findObject(getInstallButtonSelector());
+        if (install == null) {
+            throw new UnknownUiException("Could not find an install button.");
+        }
+        install.click();
+
+        // Search for the Android 6.0 permission dialog
+        if (mDevice.wait(Until.hasObject(getAndroid6DialogSelector()), 2500)) {
+            mDevice.findObject(getPositiveButtonSelector()).click();
+            mDevice.waitForIdle();
+        } else {
+            // If the install button is present, then downloading failed
+            if (mDevice.wait(Until.findObject(
+                    getInstallButtonSelector()), INSTALL_DELAY) != null) {
+                throw new UnknownUiException("Did not detect that the installation started.");
             }
         }
-        throw new UnknownUiException("Failed to find section header.");
     }
 
-    private UiObject2 getSearchBox() {
-        UiObject2 searchBox = mDevice.findObject(By.res(UI_PACKAGE, "search_box_idle_text"));
-        if (searchBox == null) {
-            searchBox = mDevice.findObject(By.res(UI_PACKAGE, "search_button"));
-        }
-        return searchBox;
-    }
-
-    private UiObject2 getScrollContainer() {
+    private boolean scrollPage(Direction dir, float value) {
         UiObject2 scroller = mDevice.findObject(By.res(UI_PACKAGE, "recycler_view"));
         if (scroller == null) {
             scroller = mDevice.findObject(By.res(UI_PACKAGE, "viewpager"));
         }
-        return scroller;
+
+        if (scroller != null) {
+            return scroller.scroll(dir, value);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isAppInstalled() {
+        if (!isAppPage()) {
+            throw new IllegalStateException("Play Store was not on the app's install page.");
+        }
+        return (mDevice.findObject(getInstallButtonSelector()) == null);
+    }
+
+    private boolean isAppPage () {
+        // Warning: this can fail if the page is scrolled down
+        return mDevice.hasObject(getAppTitleSelector());
+    }
+
+    private BySelector getPositiveButtonSelector() {
+        return By.res(UI_PACKAGE, "positive_button");
+    }
+
+    private BySelector getSearchBoxSelector() {
+        return By.res(UI_PACKAGE, "search_box_idle_text");
+    }
+
+    private BySelector getSearchButtonSelector() {
+        return By.res(UI_PACKAGE, "search_button");
+    }
+
+    private BySelector getSearchResultSelector() {
+        return By.res(UI_PACKAGE, "search_results_list");
+    }
+
+    private BySelector getAppTitleSelector() {
+        return By.res(UI_PACKAGE, "title_background");
+    }
+
+    private BySelector getInstallButtonSelector() {
+        return By.res(UI_PACKAGE, "buy_button");
+    }
+
+    private BySelector getAndroid6DialogSelector() {
+        return By.res(UI_PACKAGE, "optional_permissions_help_screen");
     }
 }