/*
* Copyright (C) 2014 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 androidx.test.uiautomator;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.annotation.NonNull;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
/**
* A {@link BySelector} specifies criteria for matching UI elements during a call to
* {@link UiDevice#findObject(BySelector)}.
*/
public class BySelector {
// Regex patterns for String criteria
Pattern mClazz;
Pattern mDesc;
Pattern mPkg;
Pattern mRes;
Pattern mText;
// Boolean criteria
Boolean mChecked;
Boolean mCheckable;
Boolean mClickable;
Boolean mEnabled;
Boolean mFocused;
Boolean mFocusable;
Boolean mLongClickable;
Boolean mScrollable;
Boolean mSelected;
// Depth restrictions
Integer mMinDepth;
Integer mMaxDepth;
// Child selectors
List<BySelector> mChildSelectors = new LinkedList<BySelector>();
/** Clients should not instanciate this class directly. Use the {@link By} factory class instead. */
BySelector() { }
/**
* Constructs a new {@link BySelector} and copies the criteria from {@code original}.
*
* @param original The {@link BySelector} to copy.
*/
BySelector(BySelector original) {
mClazz = original.mClazz;
mDesc = original.mDesc;
mPkg = original.mPkg;
mRes = original.mRes;
mText = original.mText;
mChecked = original.mChecked;
mCheckable = original.mCheckable;
mClickable = original.mClickable;
mEnabled = original.mEnabled;
mFocused = original.mFocused;
mFocusable = original.mFocusable;
mLongClickable = original.mLongClickable;
mScrollable = original.mScrollable;
mSelected = original.mSelected;
for (BySelector childSelector : original.mChildSelectors) {
mChildSelectors.add(new BySelector(childSelector));
}
}
/**
* Sets the class name criteria for matching. A UI element will be considered a match if its
* class name exactly matches the {@code className} parameter and all other criteria for
* this selector are met. If {@code className} starts with a period, it is assumed to be in the
* {@link android.widget} package.
*
* @param className The full class name value to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector clazz(@NonNull String className) {
checkNotNull(className, "className cannot be null");
// If className starts with a period, assume the package is 'android.widget'
if (className.charAt(0) == '.') {
return clazz("android.widget", className.substring(1));
} else {
return clazz(Pattern.compile(Pattern.quote(className)));
}
}
/**
* Sets the class name criteria for matching. A UI element will be considered a match if its
* package and class name exactly match the {@code packageName} and {@code className} parameters
* and all other criteria for this selector are met.
*
* @param packageName The package value to match.
* @param className The class name value to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector clazz(@NonNull String packageName, @NonNull String className) {
checkNotNull(packageName, "packageName cannot be null");
checkNotNull(className, "className cannot be null");
return clazz(Pattern.compile(Pattern.quote(
String.format("%s.%s", packageName, className))));
}
/**
* Sets the class name criteria for matching. A UI element will be considered a match if its
* class name matches {@code clazz} and all other criteria for this selector are met.
*
* @param clazz The class to match.
* @return A reference to this {@link BySelector}
*/
public @NonNull BySelector clazz(@NonNull Class clazz) {
checkNotNull(clazz, "clazz cannot be null");
return clazz(Pattern.compile(Pattern.quote(clazz.getName())));
}
/**
* Sets the class name criteria for matching. A UI element will be considered a match if its
* full class name matches the {@code className} {@link Pattern} and all other criteria for this
* selector are met.
*
* @param className The {@link Pattern} to be used for matching.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector clazz(@NonNull Pattern className) {
checkNotNull(className, "className cannot be null");
if (mClazz != null) {
throw new IllegalStateException("Class selector is already defined");
}
mClazz = className;
return this;
}
/**
* Sets the content description criteria for matching. A UI element will be considered a match
* if its content description exactly matches the {@code contentDescription} parameter and all
* other criteria for this selector are met.
*
* @param contentDescription The exact value to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector desc(@NonNull String contentDescription) {
checkNotNull(contentDescription, "contentDescription cannot be null");
return desc(Pattern.compile(Pattern.quote(contentDescription)));
}
/**
* Sets the content description criteria for matching. A UI element will be considered a match
* if its content description contains the {@code substring} parameter and all other criteria
* for this selector are met.
*
* @param substring The substring to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector descContains(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
return desc(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring))));
}
/**
* Sets the content description criteria for matching. A UI element will be considered a match
* if its content description starts with the {@code substring} parameter and all other criteria
* for this selector are met.
*
* @param substring The substring to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector descStartsWith(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
return desc(Pattern.compile(String.format("^%s.*$", Pattern.quote(substring))));
}
/**
* Sets the content description criteria for matching. A UI element will be considered a match
* if its content description ends with the {@code substring} parameter and all other criteria
* for this selector are met.
*
* @param substring The substring to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector descEndsWith(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
return desc(Pattern.compile(String.format("^.*%s$", Pattern.quote(substring))));
}
/**
* Sets the content description criteria for matching. A UI element will be considered a match
* if its content description matches the {@code contentDescription} {@link Pattern} and all
* other criteria for this selector are met.
*
* @param contentDescription The {@link Pattern} to be used for matching.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector desc(@NonNull Pattern contentDescription) {
checkNotNull(contentDescription, "contentDescription cannot be null");
if (mDesc != null) {
throw new IllegalStateException("Description selector is already defined");
}
mDesc = contentDescription;
return this;
}
/**
* Sets the application package name criteria for matching. A UI element will be considered a
* match if its application package name exactly matches the {@code applicationPackage}
* parameter and all other criteria for this selector are met.
*
* @param applicationPackage The exact value to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector pkg(@NonNull String applicationPackage) {
checkNotNull(applicationPackage, "applicationPackage cannot be null");
return pkg(Pattern.compile(Pattern.quote(applicationPackage)));
}
/**
* Sets the package name criteria for matching. A UI element will be considered a match if its
* application package name matches the {@code applicationPackage} {@link Pattern} and all other
* criteria for this selector are met.
*
* @param applicationPackage The {@link Pattern} to be used for matching.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector pkg(@NonNull Pattern applicationPackage) {
checkNotNull(applicationPackage, "applicationPackage cannot be null");
if (mPkg != null) {
throw new IllegalStateException("Package selector is already defined");
}
mPkg = applicationPackage;
return this;
}
/**
* Sets the resource name criteria for matching. A UI element will be considered a match if its
* resource name exactly matches the {@code resourceName} parameter and all other criteria for
* this selector are met.
*
* @param resourceName The exact value to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector res(@NonNull String resourceName) {
checkNotNull(resourceName, "resourceName cannot be null");
return res(Pattern.compile(Pattern.quote(resourceName)));
}
/**
* Sets the resource name criteria for matching. A UI element will be considered a match if its
* resource package and resource id exactly match the {@code resourcePackage} and
* {@code resourceId} parameters and all other criteria for this selector are met.
*
* @param resourcePackage The resource package value to match.
* @param resourceId The resouce-id value to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector res(@NonNull String resourcePackage, @NonNull String resourceId) {
checkNotNull(resourcePackage, "resourcePackage cannot be null");
checkNotNull(resourceId, "resourceId cannot be null");
return res(Pattern.compile(Pattern.quote(
String.format("%s:id/%s", resourcePackage, resourceId))));
}
/**
* Sets the resource name criteria for matching. A UI element will be considered a match if its
* resource name matches the {@code resourceName} {@link Pattern} and all other criteria for
* this selector are met.
*
* @param resourceName The {@link Pattern} to be used for matching.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector res(@NonNull Pattern resourceName) {
checkNotNull(resourceName, "resourceName cannot be null");
if (mRes != null) {
throw new IllegalStateException("Resource name selector is already defined");
}
mRes = resourceName;
return this;
}
/**
* Sets the text value criteria for matching. A UI element will be considered a match if its
* text value exactly matches the {@code textValue} parameter and all other criteria for this
* selector are met.
*
* @param textValue The exact value to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector text(@NonNull String textValue) {
checkNotNull(textValue, "textValue cannot be null");
return text(Pattern.compile(Pattern.quote(textValue)));
}
/**
* Sets the text value criteria for matching. A UI element will be considered a match if its
* text value contains the {@code substring} parameter and all other criteria for this selector
* are met.
*
* @param substring The substring to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector textContains(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
return text(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring))));
}
/**
* Sets the text value criteria for matching. A UI element will be considered a match if its
* text value starts with the {@code substring} parameter and all other criteria for this
* selector are met.
*
* @param substring The substring to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector textStartsWith(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
return text(Pattern.compile(String.format("^%s.*$", Pattern.quote(substring))));
}
/**
* Sets the text value criteria for matching. A UI element will be considered a match if its
* text value ends with the {@code substring} parameter and all other criteria for this selector
* are met.
*
* @param substring The substring to match.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector textEndsWith(@NonNull String substring) {
checkNotNull(substring, "substring cannot be null");
return text(Pattern.compile(String.format("^.*%s$", Pattern.quote(substring))));
}
/** Sets the text value criteria for matching. A UI element will be considered a match if its
* text value matches the {@code textValue} {@link Pattern} and all other criteria for this
* selector are met.
*
* @param textValue The {@link Pattern} to be used for matching.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector text(@NonNull Pattern textValue) {
checkNotNull(textValue, "textValue cannot be null");
if (mText != null) {
throw new IllegalStateException("Text selector is already defined");
}
mText = textValue;
return this;
}
/**
* Sets the search criteria to match elements that are checkable or not checkable.
*
* @param isCheckable Whether to match elements that are checkable or elements that are not
* checkable.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector checkable(boolean isCheckable) {
if (mCheckable != null) {
throw new IllegalStateException("Checkable selector is already defined");
}
mCheckable = isCheckable;
return this;
}
/**
* Sets the search criteria to match elements that are checked or unchecked.
*
* @param isChecked Whether to match elements that are checked or elements that are unchecked.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector checked(boolean isChecked) {
if (mChecked != null) {
throw new IllegalStateException("Checked selector is already defined");
}
mChecked = isChecked;
return this;
}
/**
* Sets the search criteria to match elements that are clickable or not clickable.
*
* @param isClickable Whether to match elements that are clickable or elements that are not
* clickable.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector clickable(boolean isClickable) {
if (mClickable != null) {
throw new IllegalStateException("Clickable selector is already defined");
}
mClickable = isClickable;
return this;
}
/**
* Sets the search criteria to match elements that are enabled or disabled.
*
* @param isEnabled Whether to match elements that are enabled or elements that are disabled.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector enabled(boolean isEnabled) {
if (mEnabled != null) {
throw new IllegalStateException("Enabled selector is already defined");
}
mEnabled = isEnabled;
return this;
}
/**
* Sets the search criteria to match elements that are focusable or not focusable.
*
* @param isFocusable Whether to match elements that are focusable or elements that are not
* focusable.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector focusable(boolean isFocusable) {
if (mFocusable != null) {
throw new IllegalStateException("Focusable selector is already defined");
}
mFocusable = isFocusable;
return this;
}
/**
* Sets the search criteria to match elements that are focused or unfocused.
*
* @param isFocused Whether to match elements that are focused or elements that are unfocused.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector focused(boolean isFocused) {
if (mFocused != null) {
throw new IllegalStateException("Focused selector is already defined");
}
mFocused = isFocused;
return this;
}
/**
* Sets the search criteria to match elements that are long clickable or not long clickable.
*
* @param isLongClickable Whether to match elements that are long clickable or elements that are
* not long clickable.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector longClickable(boolean isLongClickable) {
if (mLongClickable != null) {
throw new IllegalStateException("Long Clickable selector is already defined");
}
mLongClickable = isLongClickable;
return this;
}
/**
* Sets the search criteria to match elements that are scrollable or not scrollable.
*
* @param isScrollable Whether to match elements that are scrollable or elements that are not
* scrollable.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector scrollable(boolean isScrollable) {
if (mScrollable != null) {
throw new IllegalStateException("Scrollable selector is already defined");
}
mScrollable = isScrollable;
return this;
}
/**
* Sets the search criteria to match elements that are selected or not selected.
*
* @param isSelected Whether to match elements that are selected or elements that are not
* selected.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector selected(boolean isSelected) {
if (mSelected != null) {
throw new IllegalStateException("Selected selector is already defined");
}
mSelected = isSelected;
return this;
}
/** Sets the search criteria to match elements that are at a certain depth. */
public @NonNull BySelector depth(int exactDepth) {
return depth(exactDepth, exactDepth);
}
/** Sets the search criteria to match elements that are in a range of depths. */
public @NonNull BySelector depth(int min, int max) {
if (min < 0) {
throw new IllegalArgumentException("min cannot be negative");
}
if (max < 0) {
throw new IllegalArgumentException("max cannot be negative");
}
if (mMinDepth != null) {
throw new IllegalStateException("Minimum Depth selector is already defined");
}
if (mMaxDepth != null) {
throw new IllegalStateException("Maximum Depth selector is already defined");
}
mMinDepth = min;
mMaxDepth = max;
return this;
}
/** Sets the search criteria to match elements that are at least a certain depth. */
public @NonNull BySelector minDepth(int min) {
if (min < 0) {
throw new IllegalArgumentException("min cannot be negative");
}
if (mMinDepth != null) {
throw new IllegalStateException("Depth selector is already defined");
}
mMinDepth = min;
return this;
}
/** Sets the search criteria to match elements that are no more than a certain depth. */
public @NonNull BySelector maxDepth(int max) {
if (max < 0) {
throw new IllegalArgumentException("max cannot be negative");
}
if (mMaxDepth != null) {
throw new IllegalStateException("Depth selector is already defined");
}
mMaxDepth = max;
return this;
}
/**
* Adds a child selector criteria for matching. A UI element will be considered a match if it
* has a child element (direct descendant) which matches the {@code childSelector} and all
* other criteria for this selector are met. If specified more than once, matches must be found
* for all {@code childSelector}s.
*
* @param childSelector The selector used to find a matching child element.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector hasChild(@NonNull BySelector childSelector) {
checkNotNull(childSelector, "childSelector cannot be null");
return hasDescendant(childSelector, 1);
}
/**
* Adds a descendant selector criteria for matching. A UI element will be considered a match if
* it has a descendant element which matches the {@code descendantSelector} and all other
* criteria for this selector are met. If specified more than once, matches must be found for
* all {@code descendantSelector}s.
*
* @param descendantSelector The selector used to find a matching descendant element.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector hasDescendant(@NonNull BySelector descendantSelector) {
checkNotNull(descendantSelector, "descendantSelector cannot be null");
mChildSelectors.add(descendantSelector);
return this;
}
/**
* Adds a descendant selector criteria for matching. A UI element will be considered a match if
* it has a descendant element which matches the {@code descendantSelector} and all other
* criteria for this selector are met. If specified more than once, matches must be found for
* all {@code descendantSelector}s.
*
* @param descendantSelector The selector used to find a matching descendant element.
* @param maxDepth The maximum depth under the element to search the descendant.
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector hasDescendant(@NonNull BySelector descendantSelector, int maxDepth) {
checkNotNull(descendantSelector, "descendantSelector cannot be null");
descendantSelector.mMaxDepth = maxDepth;
mChildSelectors.add(descendantSelector);
return this;
}
/**
* Returns a {@link String} representation of this {@link BySelector}. The format is
* "BySelector [<KEY>='<VALUE> ... ]". Each criteria is listed as a key-value pair
* where the key is the name of the criteria expressed in all caps (e.g. CLAZZ, RES, etc).
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder("BySelector [");
if (mClazz != null) {
builder.append("CLASS='").append(mClazz).append("', ");
}
if (mDesc != null) {
builder.append("DESC='").append(mDesc).append("', ");
}
if (mPkg != null) {
builder.append("PKG='").append(mPkg).append("', ");
}
if (mRes != null) {
builder.append("RES='").append(mRes).append("', ");
}
if (mText != null) {
builder.append("TEXT='").append(mText).append("', ");
}
if (mChecked != null) {
builder.append("CHECKED='").append(mChecked).append("', ");
}
if (mCheckable != null) {
builder.append("CHECKABLE='").append(mCheckable).append("', ");
}
if (mClickable != null) {
builder.append("CLICKABLE='").append(mClickable).append("', ");
}
if (mEnabled != null) {
builder.append("ENABLED='").append(mEnabled).append("', ");
}
if (mFocused != null) {
builder.append("FOCUSED='").append(mFocused).append("', ");
}
if (mFocusable != null) {
builder.append("FOCUSABLE='").append(mFocusable).append("', ");
}
if (mLongClickable != null) {
builder.append("LONGCLICKABLE='").append(mLongClickable).append("', ");
}
if (mScrollable != null) {
builder.append("SCROLLABLE='").append(mScrollable).append("', ");
}
if (mSelected != null) {
builder.append("SELECTED='").append(mSelected).append("', ");
}
for (BySelector childSelector : mChildSelectors) {
builder.append("CHILD='").append(childSelector.toString().substring(11)).append("', ");
}
builder.setLength(builder.length() - 2);
builder.append("]");
return builder.toString();
}
private static <T> T checkNotNull(T value, @NonNull String message) {
if (value == null) {
throw new NullPointerException(message);
}
return value;
}
}