package com.robotium.solo; import android.app.Activity; import android.app.Instrumentation; import android.app.Instrumentation.ActivityMonitor; import android.content.IntentFilter; import android.os.SystemClock; import android.view.View; import android.widget.TextView; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; /** * Contains various wait methods. Examples are: waitForText(), * waitForView(). * * @author Renas Reda, renas.reda@robotium.com * */ class Waiter { private final ActivityUtils activityUtils; private final ViewFetcher viewFetcher; private final Searcher searcher; private final Scroller scroller; private final Sleeper sleeper; private final Instrumentation instrumentation; /** * Constructs this object. * * @param instrumentation the {@code Instrumentation} object * @param activityUtils the {@code ActivityUtils} instance * @param viewFetcher the {@code ViewFetcher} instance * @param searcher the {@code Searcher} instance * @param scroller the {@code Scroller} instance * @param sleeper the {@code Sleeper} instance */ public Waiter(Instrumentation instrumentation, ActivityUtils activityUtils, ViewFetcher viewFetcher, Searcher searcher, Scroller scroller, Sleeper sleeper){ this.instrumentation = instrumentation; this.activityUtils = activityUtils; this.viewFetcher = viewFetcher; this.searcher = searcher; this.scroller = scroller; this.sleeper = sleeper; } /** * Waits for the given {@link Activity}. * * @param name the name of the {@code Activity} to wait for e.g. {@code "MyActivity"} * @return {@code true} if {@code Activity} appears before the timeout and {@code false} if it does not * */ public boolean waitForActivity(String name){ return waitForActivity(name, Timeout.getSmallTimeout()); } /** * Waits for the given {@link Activity}. * * @param name the name of the {@code Activity} to wait for e.g. {@code "MyActivity"} * @param timeout the amount of time in milliseconds to wait * @return {@code true} if {@code Activity} appears before the timeout and {@code false} if it does not * */ public boolean waitForActivity(String name, int timeout){ if(isActivityMatching(activityUtils.getCurrentActivity(false, false), name)){ return true; } boolean foundActivity = false; ActivityMonitor activityMonitor = getActivityMonitor(); long currentTime = SystemClock.uptimeMillis(); final long endTime = currentTime + timeout; while(currentTime < endTime){ Activity currentActivity = activityMonitor.waitForActivityWithTimeout(endTime - currentTime); if(isActivityMatching(currentActivity, name)){ foundActivity = true; break; } currentTime = SystemClock.uptimeMillis(); } removeMonitor(activityMonitor); return foundActivity; } /** * Compares Activity names. * * @param currentActivity the Activity that is currently active * @param name the name to compare * * @return true if the Activity names match */ private boolean isActivityMatching(Activity currentActivity, String name){ if(currentActivity != null && currentActivity.getClass().getSimpleName().equals(name)) { return true; } return false; } /** * Waits for the given {@link Activity}. * * @param activityClass the class of the {@code Activity} to wait for * @return {@code true} if {@code Activity} appears before the timeout and {@code false} if it does not * */ public boolean waitForActivity(Class<? extends Activity> activityClass){ return waitForActivity(activityClass, Timeout.getSmallTimeout()); } /** * Waits for the given {@link Activity}. * * @param activityClass the class of the {@code Activity} to wait for * @param timeout the amount of time in milliseconds to wait * @return {@code true} if {@code Activity} appears before the timeout and {@code false} if it does not * */ public boolean waitForActivity(Class<? extends Activity> activityClass, int timeout){ if(isActivityMatching(activityClass, activityUtils.getCurrentActivity(false, false))){ return true; } boolean foundActivity = false; ActivityMonitor activityMonitor = getActivityMonitor(); long currentTime = SystemClock.uptimeMillis(); final long endTime = currentTime + timeout; while(currentTime < endTime){ Activity currentActivity = activityMonitor.waitForActivityWithTimeout(endTime - currentTime); if(currentActivity != null && currentActivity.getClass().equals(activityClass)) { foundActivity = true; break; } currentTime = SystemClock.uptimeMillis(); } removeMonitor(activityMonitor); return foundActivity; } /** * Compares Activity classes. * * @param activityClass the Activity class to compare * @param currentActivity the Activity that is currently active * * @return true if Activity classes match */ private boolean isActivityMatching(Class<? extends Activity> activityClass, Activity currentActivity){ if(currentActivity != null && currentActivity.getClass().equals(activityClass)) { return true; } return false; } /** * Creates a new ActivityMonitor and returns it * * @return an ActivityMonitor */ private ActivityMonitor getActivityMonitor(){ IntentFilter filter = null; ActivityMonitor activityMonitor = instrumentation.addMonitor(filter, null, false); return activityMonitor; } /** * Removes the AcitivityMonitor * * @param activityMonitor the ActivityMonitor to remove */ private void removeMonitor(ActivityMonitor activityMonitor){ try{ instrumentation.removeMonitor(activityMonitor); }catch (Exception ignored) {} } /** * Waits for a view to be shown. * * @param viewClass the {@code View} class to wait for * @param index the index of the view that is expected to be shown * @param sleep true if should sleep * @param scroll {@code true} if scrolling should be performed * @return {@code true} if view is shown and {@code false} if it is not shown before the timeout */ public <T extends View> boolean waitForView(final Class<T> viewClass, final int index, boolean sleep, boolean scroll){ Set<T> uniqueViews = new HashSet<T>(); boolean foundMatchingView; while(true){ if(sleep) sleeper.sleep(); foundMatchingView = searcher.searchFor(uniqueViews, viewClass, index); if(foundMatchingView) return true; if(scroll && !scroller.scrollDown()) return false; if(!scroll) return false; } } /** * Waits for a view to be shown. * * @param viewClass the {@code View} class to wait for * @param index the index of the view that is expected to be shown. * @param timeout the amount of time in milliseconds to wait * @param scroll {@code true} if scrolling should be performed * @return {@code true} if view is shown and {@code false} if it is not shown before the timeout */ public <T extends View> boolean waitForView(final Class<T> viewClass, final int index, final int timeout, final boolean scroll){ Set<T> uniqueViews = new HashSet<T>(); final long endTime = SystemClock.uptimeMillis() + timeout; boolean foundMatchingView; while (SystemClock.uptimeMillis() < endTime) { sleeper.sleep(); foundMatchingView = searcher.searchFor(uniqueViews, viewClass, index); if(foundMatchingView) return true; if(scroll) scroller.scrollDown(); } return false; } /** * Waits for two views to be shown. * * @param scrollMethod {@code true} if it's a method used for scrolling * @param classes the classes to wait for * @return {@code true} if any of the views are shown and {@code false} if none of the views are shown before the timeout */ public <T extends View> boolean waitForViews(boolean scrollMethod, Class<? extends T>... classes) { final long endTime = SystemClock.uptimeMillis() + Timeout.getSmallTimeout(); while (SystemClock.uptimeMillis() < endTime) { for (Class<? extends T> classToWaitFor : classes) { if (waitForView(classToWaitFor, 0, false, false)) { return true; } } if(scrollMethod){ scroller.scroll(Scroller.DOWN); } else { scroller.scrollDown(); } sleeper.sleep(); } return false; } /** * Waits for a given view. Default timeout is 20 seconds. * * @param view the view to wait for * @return {@code true} if view is shown and {@code false} if it is not shown before the timeout */ public boolean waitForView(View view){ View viewToWaitFor = waitForView(view, Timeout.getLargeTimeout(), true, true); if(viewToWaitFor != null) { return true; } return false; } /** * Waits for a given view. * * @param view the view to wait for * @param timeout the amount of time in milliseconds to wait * @return {@code true} if view is shown and {@code false} if it is not shown before the timeout */ public View waitForView(View view, int timeout){ return waitForView(view, timeout, true, true); } /** * Waits for a given view. * * @param view the view to wait for * @param timeout the amount of time in milliseconds to wait * @param scroll {@code true} if scrolling should be performed * @param checkIsShown {@code true} if view.isShown() should be used * @return {@code true} if view is shown and {@code false} if it is not shown before the timeout */ public View waitForView(View view, int timeout, boolean scroll, boolean checkIsShown){ long endTime = SystemClock.uptimeMillis() + timeout; int retry = 0; if(view == null) return null; while (SystemClock.uptimeMillis() < endTime) { final boolean foundAnyMatchingView = searcher.searchFor(view); if(checkIsShown && foundAnyMatchingView && !view.isShown()){ sleeper.sleepMini(); retry++; View identicalView = viewFetcher.getIdenticalView(view); if(identicalView != null && !view.equals(identicalView)){ view = identicalView; } if(retry > 5){ return view; } continue; } if (foundAnyMatchingView){ return view; } if(scroll) { scroller.scrollDown(); } sleeper.sleep(); } return view; } /** * Waits for a certain view. * * @param id the id of the view to wait for * @param index the index of the {@link View}. {@code 0} if only one is available * @param timeout the timeout in milliseconds * @return the specified View */ public View waitForView(int id, int index, int timeout){ if(timeout == 0){ timeout = Timeout.getSmallTimeout(); } return waitForView(id, index, timeout, false); } /** * Waits for a certain view. * * @param id the id of the view to wait for * @param index the index of the {@link View}. {@code 0} if only one is available * @return the specified View */ public View waitForView(int id, int index, int timeout, boolean scroll){ Set<View> uniqueViewsMatchingId = new HashSet<View>(); long endTime = SystemClock.uptimeMillis() + timeout; while (SystemClock.uptimeMillis() <= endTime) { sleeper.sleep(); for (View view : viewFetcher.getAllViews(false)) { Integer idOfView = Integer.valueOf(view.getId()); if (idOfView.equals(id)) { uniqueViewsMatchingId.add(view); if(uniqueViewsMatchingId.size() > index) { return view; } } } if(scroll) scroller.scrollDown(); } return null; } /** * Waits for a certain view. * * @param tag the tag of the view to wait for * @param index the index of the {@link View}. {@code 0} if only one is available * @param timeout the timeout in milliseconds * @return the specified View */ public View waitForView(Object tag, int index, int timeout){ if(timeout == 0){ timeout = Timeout.getSmallTimeout(); } return waitForView(tag, index, timeout, false); } /** * Waits for a certain view. * * @param tag the tag of the view to wait for * @param index the index of the {@link View}. {@code 0} if only one is available * @return the specified View */ public View waitForView(Object tag, int index, int timeout, boolean scroll){ //Because https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/View.java#L17005-L17007 if(tag == null) { return null; } Set<View> uniqueViewsMatchingId = new HashSet<View>(); long endTime = SystemClock.uptimeMillis() + timeout; while (SystemClock.uptimeMillis() <= endTime) { sleeper.sleep(); for (View view : viewFetcher.getAllViews(false)) { if (tag.equals(view.getTag())) { uniqueViewsMatchingId.add(view); if(uniqueViewsMatchingId.size() > index) { return view; } } } if(scroll) { scroller.scrollDown(); } } return null; } /** * Waits for a web element. * * @param by the By object. Examples are By.id("id") and By.name("name") * @param minimumNumberOfMatches the minimum number of matches that are expected to be shown. {@code 0} means any number of matches * @param timeout the the amount of time in milliseconds to wait * @param scroll {@code true} if scrolling should be performed */ public WebElement waitForWebElement(final By by, int minimumNumberOfMatches, int timeout, boolean scroll){ final long endTime = SystemClock.uptimeMillis() + timeout; while (true) { final boolean timedOut = SystemClock.uptimeMillis() > endTime; if (timedOut){ searcher.logMatchesFound(by.getValue()); return null; } sleeper.sleep(); WebElement webElementToReturn = searcher.searchForWebElement(by, minimumNumberOfMatches); if(webElementToReturn != null) return webElementToReturn; if(scroll) { scroller.scrollDown(); } } } /** * Waits for a condition to be satisfied. * * @param condition the condition to wait for * @param timeout the amount of time in milliseconds to wait * @return {@code true} if condition is satisfied and {@code false} if it is not satisfied before the timeout */ public boolean waitForCondition(Condition condition, int timeout){ final long endTime = SystemClock.uptimeMillis() + timeout; while (true) { final boolean timedOut = SystemClock.uptimeMillis() > endTime; if (timedOut){ return false; } sleeper.sleep(); if (condition.isSatisfied()){ return true; } } } /** * Waits for a text to be shown. Default timeout is 20 seconds. * * @param text the text that needs to be shown, specified as a regular expression * @return {@code true} if text is found and {@code false} if it is not found before the timeout */ public TextView waitForText(String text) { return waitForText(text, 0, Timeout.getLargeTimeout(), true); } /** * Waits for a text to be shown. * * @param text the text that needs to be shown, specified as a regular expression * @param expectedMinimumNumberOfMatches the minimum number of matches of text that must be shown. {@code 0} means any number of matches * @param timeout the amount of time in milliseconds to wait * @return {@code true} if text is found and {@code false} if it is not found before the timeout */ public TextView waitForText(String text, int expectedMinimumNumberOfMatches, long timeout) { return waitForText(text, expectedMinimumNumberOfMatches, timeout, true); } /** * Waits for a text to be shown. * * @param text the text that needs to be shown, specified as a regular expression * @param expectedMinimumNumberOfMatches the minimum number of matches of text that must be shown. {@code 0} means any number of matches * @param timeout the amount of time in milliseconds to wait * @param scroll {@code true} if scrolling should be performed * @return {@code true} if text is found and {@code false} if it is not found before the timeout */ public TextView waitForText(String text, int expectedMinimumNumberOfMatches, long timeout, boolean scroll) { return waitForText(TextView.class, text, expectedMinimumNumberOfMatches, timeout, scroll, false, true); } /** * Waits for a text to be shown. * * @param classToFilterBy the class to filter by * @param text the text that needs to be shown, specified as a regular expression * @param expectedMinimumNumberOfMatches the minimum number of matches of text that must be shown. {@code 0} means any number of matches * @param timeout the amount of time in milliseconds to wait * @param scroll {@code true} if scrolling should be performed * @return {@code true} if text is found and {@code false} if it is not found before the timeout */ public <T extends TextView> T waitForText(Class<T> classToFilterBy, String text, int expectedMinimumNumberOfMatches, long timeout, boolean scroll) { return waitForText(classToFilterBy, text, expectedMinimumNumberOfMatches, timeout, scroll, false, true); } /** * Waits for a text to be shown. * * @param text the text that needs to be shown, specified as a regular expression. * @param expectedMinimumNumberOfMatches the minimum number of matches of text that must be shown. {@code 0} means any number of matches * @param timeout the amount of time in milliseconds to wait * @param scroll {@code true} if scrolling should be performed * @param onlyVisible {@code true} if only visible text views should be waited for * @param hardStoppage {@code true} if search is to be stopped when timeout expires * @return {@code true} if text is found and {@code false} if it is not found before the timeout */ public TextView waitForText(String text, int expectedMinimumNumberOfMatches, long timeout, boolean scroll, boolean onlyVisible, boolean hardStoppage) { return waitForText(TextView.class, text, expectedMinimumNumberOfMatches, timeout, scroll, onlyVisible, hardStoppage); } /** * Waits for a text to be shown. * * @param classToFilterBy the class to filter by * @param text the text that needs to be shown, specified as a regular expression. * @param expectedMinimumNumberOfMatches the minimum number of matches of text that must be shown. {@code 0} means any number of matches * @param timeout the amount of time in milliseconds to wait * @param scroll {@code true} if scrolling should be performed * @param onlyVisible {@code true} if only visible text views should be waited for * @param hardStoppage {@code true} if search is to be stopped when timeout expires * @return {@code true} if text is found and {@code false} if it is not found before the timeout */ public <T extends TextView> T waitForText(Class<T> classToFilterBy, String text, int expectedMinimumNumberOfMatches, long timeout, boolean scroll, boolean onlyVisible, boolean hardStoppage) { final long endTime = SystemClock.uptimeMillis() + timeout; while (true) { final boolean timedOut = SystemClock.uptimeMillis() > endTime; if (timedOut){ return null; } sleeper.sleep(); if(!hardStoppage) timeout = 0; final T textViewToReturn = searcher.searchFor(classToFilterBy, text, expectedMinimumNumberOfMatches, timeout, scroll, onlyVisible); if (textViewToReturn != null ){ return textViewToReturn; } } } /** * Waits for and returns a View. * * @param index the index of the view * @param classToFilterBy the class to filter * @return the specified View */ public <T extends View> T waitForAndGetView(int index, Class<T> classToFilterBy){ long endTime = SystemClock.uptimeMillis() + Timeout.getSmallTimeout(); while (SystemClock.uptimeMillis() <= endTime && !waitForView(classToFilterBy, index, true, true)); int numberOfUniqueViews = searcher.getNumberOfUniqueViews(); ArrayList<T> views = RobotiumUtils.removeInvisibleViews(viewFetcher.getCurrentViews(classToFilterBy, true)); if(views.size() < numberOfUniqueViews){ int newIndex = index - (numberOfUniqueViews - views.size()); if(newIndex >= 0) index = newIndex; } T view = null; try{ view = views.get(index); }catch (IndexOutOfBoundsException exception) { int match = index + 1; if(match > 1) { Log.e(Solo.LOG_TAG, match + " " + classToFilterBy.getSimpleName() +"s" + " are not found!"); } else { Log.e(Solo.LOG_TAG, classToFilterBy.getSimpleName() + " is not found!"); } } views = null; return view; } /** * Waits for a Fragment with a given tag or id to appear. * * @param tag the name of the tag or null if no tag * @param id the id of the tag * @param timeout the amount of time in milliseconds to wait * @return true if fragment appears and false if it does not appear before the timeout */ public boolean waitForFragment(String tag, int id, int timeout){ long endTime = SystemClock.uptimeMillis() + timeout; while (SystemClock.uptimeMillis() <= endTime) { if(getFragment(tag, id) != null) return true; } return false; } /** * Waits for a log message to appear. * Requires read logs permission (android.permission.READ_LOGS) in AndroidManifest.xml of the application under test. * * @param logMessage the log message to wait for * @param timeout the amount of time in milliseconds to wait * @return true if log message appears and false if it does not appear before the timeout */ public boolean waitForLogMessage(String logMessage, int timeout){ StringBuilder stringBuilder = new StringBuilder(); long endTime = SystemClock.uptimeMillis() + timeout; while (SystemClock.uptimeMillis() <= endTime) { if(getLog(stringBuilder).lastIndexOf(logMessage) != -1){ return true; } sleeper.sleep(); } return false; } /** * Returns the log in the given stringBuilder. * * @param stringBuilder the StringBuilder object to return the log in * @return the log */ private StringBuilder getLog(StringBuilder stringBuilder) { Process p = null; BufferedReader reader = null; String line = null; try { // read output from logcat p = Runtime.getRuntime().exec("logcat -d"); reader = new BufferedReader( new InputStreamReader(p.getInputStream())); stringBuilder.setLength(0); while ((line = reader.readLine()) != null) { stringBuilder.append(line); } reader.close(); // read error from logcat StringBuilder errorLog = new StringBuilder(); reader = new BufferedReader(new InputStreamReader( p.getErrorStream())); errorLog.append("logcat returns error: "); while ((line = reader.readLine()) != null) { errorLog.append(line); } reader.close(); // Exception would be thrown if we get exitValue without waiting for the process // to finish p.waitFor(); // if exit value of logcat is non-zero, it means error if (p.exitValue() != 0) { destroy(p, reader); throw new Exception(errorLog.toString()); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } destroy(p, reader); return stringBuilder; } /** * Clears the log. */ public void clearLog(){ Process p = null; try { p = Runtime.getRuntime().exec("logcat -c"); }catch(IOException e){ e.printStackTrace(); } } /** * Destroys the process and closes the BufferedReader. * * @param p the process to destroy * @param reader the BufferedReader to close */ private void destroy(Process p, BufferedReader reader){ p.destroy(); try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } /** * Returns a Fragment with a given tag or id. * * @param tag the tag of the Fragment or null if no tag * @param id the id of the Fragment * @return a SupportFragment with a given tag or id */ private android.app.Fragment getFragment(String tag, int id){ try{ if(tag == null) return activityUtils.getCurrentActivity().getFragmentManager().findFragmentById(id); else return activityUtils.getCurrentActivity().getFragmentManager().findFragmentByTag(tag); }catch (Throwable ignored) {} return null; } }