package com.robotium.solo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import android.os.SystemClock;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
/**
* Contains various search methods. Examples are: searchForEditTextWithTimeout(),
* searchForTextWithTimeout(), searchForButtonWithTimeout().
*
* @author Renas Reda, renas.reda@robotium.com
*
*/
class Searcher {
private final ViewFetcher viewFetcher;
private final WebUtils webUtils;
private final Scroller scroller;
private final Sleeper sleeper;
private final String LOG_TAG = "Robotium";
Set<TextView> uniqueTextViews;
List<WebElement> webElements;
private int numberOfUniqueViews;
private final int TIMEOUT = 5000;
/**
* Constructs this object.
*
* @param viewFetcher the {@code ViewFetcher} instance
* @param webUtils the {@code WebUtils} instance
* @param scroller the {@code Scroller} instance
* @param sleeper the {@code Sleeper} instance.
*/
public Searcher(ViewFetcher viewFetcher, WebUtils webUtils, Scroller scroller, Sleeper sleeper) {
this.viewFetcher = viewFetcher;
this.webUtils = webUtils;
this.scroller = scroller;
this.sleeper = sleeper;
webElements = new ArrayList<WebElement>();
uniqueTextViews = new HashSet<TextView>();
}
/**
* Searches for a {@code View} with the given regex string and returns {@code true} if the
* searched {@code Button} is found a given number of times. Will automatically scroll when needed.
*
* @param viewClass what kind of {@code View} to search for, e.g. {@code Button.class} or {@code TextView.class}
* @param regex the text to search for. The parameter <strong>will</strong> be interpreted as a regular expression.
* @param expectedMinimumNumberOfMatches the minimum number of matches expected to be found. {@code 0} matches means that one or more
* matches are expected to be found
* @param scroll whether scrolling should be performed
* @param onlyVisible {@code true} if only texts visible on the screen should be searched
*
* @return {@code true} if a {@code View} of the specified class with the given text is found a given number of
* times, and {@code false} if it is not found
*/
public boolean searchWithTimeoutFor(Class<? extends TextView> viewClass, String regex, int expectedMinimumNumberOfMatches, boolean scroll, boolean onlyVisible) {
final long endTime = SystemClock.uptimeMillis() + TIMEOUT;
TextView foundAnyMatchingView = null;
while (SystemClock.uptimeMillis() < endTime) {
sleeper.sleep();
foundAnyMatchingView = searchFor(viewClass, regex, expectedMinimumNumberOfMatches, 0, scroll, onlyVisible);
if (foundAnyMatchingView !=null){
return true;
}
}
return false;
}
/**
* Searches for a {@code View} with the given regex string and returns {@code true} if the
* searched {@code View} is found a given number of times.
*
* @param viewClass what kind of {@code View} to search for, e.g. {@code Button.class} or {@code TextView.class}
* @param regex the text to search for. The parameter <strong>will</strong> be interpreted as a regular expression.
* @param expectedMinimumNumberOfMatches the minimum number of matches expected to be found. {@code 0} matches means that one or more
* matches are expected to be found.
* @param timeout the amount of time in milliseconds to wait
* @param scroll whether scrolling should be performed
* @param onlyVisible {@code true} if only texts visible on the screen should be searched
*
* @return {@code true} if a view of the specified class with the given text is found a given number of times.
* {@code false} if it is not found.
*/
public <T extends TextView> T searchFor(final Class<T> viewClass, final String regex, int expectedMinimumNumberOfMatches, final long timeout, final boolean scroll, final boolean onlyVisible) {
if(expectedMinimumNumberOfMatches < 1) {
expectedMinimumNumberOfMatches = 1;
}
final Callable<Collection<T>> viewFetcherCallback = new Callable<Collection<T>>() {
@SuppressWarnings("unchecked")
public Collection<T> call() throws Exception {
sleeper.sleep();
ArrayList<T> viewsToReturn = viewFetcher.getCurrentViews(viewClass, true);
if(onlyVisible){
viewsToReturn = RobotiumUtils.removeInvisibleViews(viewsToReturn);
}
if(viewClass.isAssignableFrom(TextView.class)) {
viewsToReturn.addAll((Collection<? extends T>) webUtils.getTextViewsFromWebView());
}
return viewsToReturn;
}
};
try {
return searchFor(viewFetcherCallback, regex, expectedMinimumNumberOfMatches, timeout, scroll);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Searches for a view class.
*
* @param uniqueViews the set of unique views
* @param viewClass the view class to search for
* @param index the index of the view class
* @return true if view class if found a given number of times
*/
public <T extends View> boolean searchFor(Set<T> uniqueViews, Class<T> viewClass, final int index) {
ArrayList<T> allViews = RobotiumUtils.removeInvisibleViews(viewFetcher.getCurrentViews(viewClass, true));
int uniqueViewsFound = (getNumberOfUniqueViews(uniqueViews, allViews));
if(uniqueViewsFound > 0 && index < uniqueViewsFound) {
return true;
}
if(uniqueViewsFound > 0 && index == 0) {
return true;
}
return false;
}
/**
* Searches for a given view.
*
* @param view the view to search
* @param scroll true if scrolling should be performed
* @return true if view is found
*/
public <T extends View> boolean searchFor(View view) {
ArrayList<View> views = viewFetcher.getAllViews(true);
for(View v : views){
if(v.equals(view)){
return true;
}
}
return false;
}
/**
* Searches for a {@code View} with the given regex string and returns {@code true} if the
* searched {@code View} is found a given number of times. Will not scroll, because the caller needs to find new
* {@code View}s to evaluate after scrolling, and call this method again.
*
* @param viewFetcherCallback callback which should return an updated collection of views to search
* @param regex the text to search for. The parameter <strong>will</strong> be interpreted as a regular expression.
* @param expectedMinimumNumberOfMatches the minimum number of matches expected to be found. {@code 0} matches means that one or more
* matches are expected to be found.
* @param timeout the amount of time in milliseconds to wait
* @param scroll whether scrolling should be performed
*
* @return {@code true} if a view of the specified class with the given text is found a given number of times.
* {@code false} if it is not found.
*
* @throws Exception not really, it's just the signature of {@code Callable}
*/
public <T extends TextView> T searchFor(Callable<Collection<T>> viewFetcherCallback, String regex, int expectedMinimumNumberOfMatches, long timeout, boolean scroll) throws Exception {
final long endTime = SystemClock.uptimeMillis() + timeout;
Collection<T> views;
while (true) {
final boolean timedOut = timeout > 0 && SystemClock.uptimeMillis() > endTime;
if(timedOut){
logMatchesFound(regex);
return null;
}
views = viewFetcherCallback.call();
for(T view : views){
if (RobotiumUtils.getNumberOfMatches(regex, view, uniqueTextViews) == expectedMinimumNumberOfMatches) {
uniqueTextViews.clear();
return view;
}
}
if(scroll && !scroller.scrollDown()){
logMatchesFound(regex);
return null;
}
if(!scroll){
logMatchesFound(regex);
return null;
}
}
}
/**
* Searches for a web element.
*
* @param by the By object e.g. By.id("id");
* @param minimumNumberOfMatches the minimum number of matches that are expected to be shown. {@code 0} means any number of matches
* @return the web element or null if not found
*/
public WebElement searchForWebElement(final By by, int minimumNumberOfMatches){
if(minimumNumberOfMatches < 1){
minimumNumberOfMatches = 1;
}
List<WebElement> viewsFromScreen = webUtils.getWebElements(by, true);
addViewsToList (webElements, viewsFromScreen);
return getViewFromList(webElements, minimumNumberOfMatches);
}
/**
* Adds views to a given list.
*
* @param allWebElements the list of all views
* @param webTextViewsOnScreen the list of views shown on screen
*/
private void addViewsToList(List<WebElement> allWebElements, List<WebElement> webElementsOnScreen){
int[] xyViewFromSet = new int[2];
int[] xyViewFromScreen = new int[2];
for(WebElement textFromScreen : webElementsOnScreen){
boolean foundView = false;
textFromScreen.getLocationOnScreen(xyViewFromScreen);
for(WebElement textFromList : allWebElements){
textFromList.getLocationOnScreen(xyViewFromSet);
if(textFromScreen.getText().equals(textFromList.getText()) && xyViewFromScreen[0] == xyViewFromSet[0] && xyViewFromScreen[1] == xyViewFromSet[1]) {
foundView = true;
}
}
if(!foundView){
allWebElements.add(textFromScreen);
}
}
}
/**
* Returns a text view with a given match.
*
* @param webElements the list of views
* @param match the match of the view to return
* @return the view with a given match
*/
private WebElement getViewFromList(List<WebElement> webElements, int match){
WebElement webElementToReturn = null;
if(webElements.size() >= match){
try{
webElementToReturn = webElements.get(--match);
}catch(Exception ignored){}
}
if(webElementToReturn != null)
webElements.clear();
return webElementToReturn;
}
/**
* Returns the number of unique views.
*
* @param uniqueViews the set of unique views
* @param views the list of all views
* @return number of unique views
*/
public <T extends View> int getNumberOfUniqueViews(Set<T>uniqueViews, ArrayList<T> views){
for(int i = 0; i < views.size(); i++){
uniqueViews.add(views.get(i));
}
numberOfUniqueViews = uniqueViews.size();
return numberOfUniqueViews;
}
/**
* Returns the number of unique views.
*
* @return the number of unique views
*/
public int getNumberOfUniqueViews(){
return numberOfUniqueViews;
}
/**
* Logs a (searchFor failed) message.
*
* @param regex the search string to log
*/
public void logMatchesFound(String regex){
if (uniqueTextViews.size() > 0) {
Log.d(LOG_TAG, " There are only " + uniqueTextViews.size() + " matches of '" + regex + "'");
}
else if(webElements.size() > 0){
Log.d(LOG_TAG, " There are only " + webElements.size() + " matches of '" + regex + "'");
}
uniqueTextViews.clear();
webElements.clear();
}
}