/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
* 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 io.appium.android.bootstrap;
import android.graphics.Rect;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiSelector;
import io.appium.android.bootstrap.exceptions.ElementNotFoundException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.regex.Pattern;
import io.appium.android.bootstrap.Logger;
/**
* A cache of elements that the app has seen.
*
*/
public class AndroidElementsHash {
private static final Pattern endsWithInstancePattern = Pattern.compile(".*INSTANCE=\\d+]$");
public static AndroidElementsHash getInstance() {
if (AndroidElementsHash.instance == null) {
AndroidElementsHash.instance = new AndroidElementsHash();
}
return AndroidElementsHash.instance;
}
private final Hashtable<String, AndroidElement> elements;
private Integer counter;
private static AndroidElementsHash instance;
/**
* Constructor
*/
public AndroidElementsHash() {
counter = 0;
elements = new Hashtable<String, AndroidElement>();
}
/**
* @param element
* @return
*/
public AndroidElement addElement(final UiObject element) {
counter++;
final String key = counter.toString();
final AndroidElement el = new AndroidElement(key, element);
elements.put(key, el);
return el;
}
/**
* Return an element given an Id.
*
* @param key
* @return {@link AndroidElement}
*/
public AndroidElement getElement(final String key) {
return elements.get(key);
}
/**
* Return an elements child given the key (context id), or uses the selector
* to get the element.
*
* @param sel
* @param key
* Element id.
* @return {@link AndroidElement}
* @throws ElementNotFoundException
*/
public AndroidElement getElement(final UiSelector sel, final String key)
throws ElementNotFoundException {
AndroidElement baseEl;
baseEl = elements.get(key);
UiObject el;
if (baseEl == null) {
el = new UiObject(sel);
} else {
try {
el = baseEl.getChild(sel);
} catch (final UiObjectNotFoundException e) {
throw new ElementNotFoundException();
}
}
if (el.exists()) {
// there are times when UiAutomator returns an element from another parent
// so we need to see if it is within the bounds of the parent
try {
if (baseEl != null && !Rect.intersects(baseEl.getBounds(), el.getBounds())) {
Logger.debug("UiAutomator returned a child element but it is " +
"outside the bounds of the parent. Assuming no " +
"child element found");
throw new ElementNotFoundException();
}
} catch (final UiObjectNotFoundException e) {
throw new ElementNotFoundException();
}
return addElement(el);
} else {
throw new ElementNotFoundException();
}
}
/**
* Same as {@link #getElement(UiSelector, String)} but for multiple elements
* at once.
*
* @param sel
* @param key
* @return ArrayList<{@link AndroidElement}>
* @throws UiObjectNotFoundException
*/
public ArrayList<AndroidElement> getElements(final UiSelector sel,
final String key) throws UiObjectNotFoundException {
boolean keepSearching = true;
final String selectorString = sel.toString();
final boolean useIndex = selectorString.contains("CLASS_REGEX=");
final boolean endsWithInstance = endsWithInstancePattern.matcher(selectorString).matches();
Logger.debug("getElements selector:" + selectorString);
final ArrayList<AndroidElement> elements = new ArrayList<AndroidElement>();
final AndroidElement baseEl = this.getElement(key);
// If sel is UiSelector[CLASS=android.widget.Button, INSTANCE=0]
// then invoking instance with a non-0 argument will corrupt the selector.
//
// sel.instance(1) will transform the selector into:
// UiSelector[CLASS=android.widget.Button, INSTANCE=1]
//
// The selector now points to an entirely different element.
if (endsWithInstance) {
Logger.debug("Selector ends with instance.");
UiObject instanceObj;
if (baseEl != null) {
instanceObj = baseEl.getChild(sel);
} else {
instanceObj = new UiObject(sel);
}
// There's exactly one element when using instance.
if (instanceObj != null && instanceObj.exists()) {
elements.add(addElement(instanceObj));
}
return elements;
}
UiObject lastFoundObj;
UiSelector tmp;
int counter = 0;
while (keepSearching) {
if (baseEl == null) {
Logger.debug("Element[" + key + "] is null: (" + counter + ")");
if (useIndex) {
Logger.debug(" using index...");
tmp = sel.index(counter);
} else {
tmp = sel.instance(counter);
}
Logger.debug("getElements tmp selector:" + tmp.toString());
lastFoundObj = new UiObject(tmp);
} else {
Logger.debug("Element[" + key + "] is " + baseEl.getId() + ", counter: "
+ counter);
lastFoundObj = baseEl.getChild(sel.instance(counter));
}
counter++;
if (lastFoundObj != null && lastFoundObj.exists()) {
elements.add(addElement(lastFoundObj));
} else {
keepSearching = false;
}
}
return elements;
}
}