1 /* 2 * Copyright (C) 2013 DroidDriver committers 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package io.appium.droiddriver.scroll; 17 18 import android.util.Log; 19 20 import java.util.List; 21 22 import io.appium.droiddriver.DroidDriver; 23 import io.appium.droiddriver.UiElement; 24 import io.appium.droiddriver.exceptions.ElementNotFoundException; 25 import io.appium.droiddriver.finders.By; 26 import io.appium.droiddriver.finders.Finder; 27 import io.appium.droiddriver.finders.Predicate; 28 import io.appium.droiddriver.finders.Predicates; 29 import io.appium.droiddriver.scroll.Direction.DirectionConverter; 30 import io.appium.droiddriver.scroll.Direction.LogicalDirection; 31 import io.appium.droiddriver.scroll.Direction.PhysicalDirection; 32 import io.appium.droiddriver.util.Logs; 33 34 /** 35 * A {@link ScrollStepStrategy} that determines whether scrolling is possible 36 * based on a sentinel. 37 */ 38 public abstract class SentinelStrategy implements ScrollStepStrategy { 39 /** 40 * A {@link Finder} for sentinel. Note that unlike {@link Finder}, invisible 41 * UiElements are not skipped by default. 42 */ 43 public static abstract class Getter implements Finder { 44 protected final Predicate<? super UiElement> predicate; 45 Getter()46 protected Getter() { 47 // Include invisible children by default. 48 this(null); 49 } 50 Getter(Predicate<? super UiElement> predicate)51 protected Getter(Predicate<? super UiElement> predicate) { 52 this.predicate = predicate; 53 } 54 55 /** 56 * Gets the sentinel, which must be an immediate child of {@code container} 57 * - not a descendant. Note sentinel may not exist if {@code container} has 58 * not finished updating. 59 */ 60 @Override find(UiElement container)61 public UiElement find(UiElement container) { 62 UiElement sentinel = getSentinel(container.getChildren(predicate)); 63 if (sentinel == null) { 64 throw new ElementNotFoundException(this); 65 } 66 Logs.log(Log.INFO, "Found sentinel: " + sentinel); 67 return sentinel; 68 } 69 70 getSentinel(List<? extends UiElement> children)71 protected abstract UiElement getSentinel(List<? extends UiElement> children); 72 73 @Override toString()74 public abstract String toString(); 75 } 76 77 /** 78 * Returns the first child as the sentinel. 79 */ 80 public static final Getter FIRST_CHILD_GETTER = new Getter() { 81 @Override 82 protected UiElement getSentinel(List<? extends UiElement> children) { 83 return children.isEmpty() ? null : children.get(0); 84 } 85 86 @Override 87 public String toString() { 88 return "FIRST_CHILD"; 89 } 90 }; 91 /** 92 * Returns the last child as the sentinel. 93 */ 94 public static final Getter LAST_CHILD_GETTER = new Getter() { 95 @Override 96 protected UiElement getSentinel(List<? extends UiElement> children) { 97 return children.isEmpty() ? null : children.get(children.size() - 1); 98 } 99 100 @Override 101 public String toString() { 102 return "LAST_CHILD"; 103 } 104 }; 105 /** 106 * Returns the second last child as the sentinel. Useful when the activity 107 * always shows the last child as an anchor (for example a footer). 108 * <p> 109 * Sometimes uiautomatorviewer may not show the anchor as the last child, due 110 * to the reordering by layout described in {@link UiElement#getChildren}. 111 * This is not a problem with UiAutomationDriver because it sees the same as 112 * uiautomatorviewer does, but could be a problem with InstrumentationDriver. 113 * </p> 114 */ 115 public static final Getter SECOND_LAST_CHILD_GETTER = new Getter() { 116 @Override 117 protected UiElement getSentinel(List<? extends UiElement> children) { 118 return children.size() < 2 ? null : children.get(children.size() - 2); 119 } 120 121 @Override 122 public String toString() { 123 return "SECOND_LAST_CHILD"; 124 } 125 }; 126 /** 127 * Returns the second child as the sentinel. Useful when the activity shows a 128 * fixed first child. 129 */ 130 public static final Getter SECOND_CHILD_GETTER = new Getter() { 131 @Override 132 protected UiElement getSentinel(List<? extends UiElement> children) { 133 return children.size() <= 1 ? null : children.get(1); 134 } 135 136 @Override 137 public String toString() { 138 return "SECOND_CHILD"; 139 } 140 }; 141 142 /** 143 * Decorates a {@link Getter} by adding another {@link Predicate}. 144 */ 145 public static class MorePredicateGetter extends Getter { 146 private final Getter original; 147 MorePredicateGetter(Getter original, Predicate<? super UiElement> extraPredicate)148 public MorePredicateGetter(Getter original, Predicate<? super UiElement> extraPredicate) { 149 super(Predicates.allOf(original.predicate, extraPredicate)); 150 this.original = original; 151 } 152 153 @Override getSentinel(List<? extends UiElement> children)154 protected UiElement getSentinel(List<? extends UiElement> children) { 155 return original.getSentinel(children); 156 } 157 158 @Override toString()159 public String toString() { 160 return predicate.toString() + " " + original; 161 } 162 } 163 164 private final Getter backwardGetter; 165 private final Getter forwardGetter; 166 private final DirectionConverter directionConverter; 167 SentinelStrategy(Getter backwardGetter, Getter forwardGetter, DirectionConverter directionConverter)168 protected SentinelStrategy(Getter backwardGetter, Getter forwardGetter, 169 DirectionConverter directionConverter) { 170 this.backwardGetter = backwardGetter; 171 this.forwardGetter = forwardGetter; 172 this.directionConverter = directionConverter; 173 } 174 getSentinel(DroidDriver driver, Finder containerFinder, PhysicalDirection direction)175 protected UiElement getSentinel(DroidDriver driver, Finder containerFinder, 176 PhysicalDirection direction) { 177 Logs.call(this, "getSentinel", driver, containerFinder, direction); 178 Finder sentinelFinder; 179 LogicalDirection logicalDirection = directionConverter.toLogicalDirection(direction); 180 if (logicalDirection == LogicalDirection.BACKWARD) { 181 sentinelFinder = By.chain(containerFinder, backwardGetter); 182 } else { 183 sentinelFinder = By.chain(containerFinder, forwardGetter); 184 } 185 return driver.on(sentinelFinder); 186 } 187 188 @Override getDirectionConverter()189 public final DirectionConverter getDirectionConverter() { 190 return directionConverter; 191 } 192 193 @Override beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, PhysicalDirection direction)194 public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, 195 PhysicalDirection direction) {} 196 197 @Override endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, PhysicalDirection direction)198 public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, 199 PhysicalDirection direction) {} 200 201 @Override toString()202 public String toString() { 203 return String.format("{backwardGetter=%s, forwardGetter=%s}", backwardGetter, forwardGetter); 204 } 205 206 @Override doScroll(UiElement container, PhysicalDirection direction)207 public void doScroll(UiElement container, PhysicalDirection direction) { 208 container.scroll(direction); 209 } 210 } 211