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 io.appium.droiddriver.DroidDriver; 21 import io.appium.droiddriver.UiElement; 22 import io.appium.droiddriver.exceptions.ElementNotFoundException; 23 import io.appium.droiddriver.finders.By; 24 import io.appium.droiddriver.finders.Finder; 25 import io.appium.droiddriver.scroll.Direction.DirectionConverter; 26 import io.appium.droiddriver.scroll.Direction.PhysicalDirection; 27 import io.appium.droiddriver.util.Logs; 28 import io.appium.droiddriver.util.Strings; 29 30 /** 31 * Determines whether scrolling is possible by checking whether the sentinel 32 * child is updated after scrolling. Use this when {@link UiElement#getChildren} 33 * is not reliable. This can happen, for instance, when UiAutomationDriver is 34 * used, which skips invisible children, or in the case of dynamic list, which 35 * shows more items when scrolling beyond the end. 36 */ 37 public class DynamicSentinelStrategy extends SentinelStrategy { 38 39 /** 40 * Interface for determining whether sentinel is updated. 41 */ 42 public static interface IsUpdatedStrategy { 43 /** 44 * Returns whether {@code newSentinel} is updated from {@code oldSentinel}. 45 */ isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel)46 boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel); 47 48 /** 49 * {@inheritDoc} 50 * 51 * <p> 52 * It is recommended that this method return a description to help 53 * debugging. 54 */ 55 @Override toString()56 String toString(); 57 } 58 59 /** 60 * Determines whether the sentinel is updated by checking a single unique 61 * String attribute of a descendant element of the sentinel (or itself). 62 */ 63 public static abstract class SingleStringUpdated implements IsUpdatedStrategy { 64 private final Finder uniqueStringFinder; 65 66 /** 67 * @param uniqueStringFinder a Finder relative to the sentinel that finds 68 * its descendant or self which contains a unique String. 69 */ SingleStringUpdated(Finder uniqueStringFinder)70 public SingleStringUpdated(Finder uniqueStringFinder) { 71 this.uniqueStringFinder = uniqueStringFinder; 72 } 73 74 /** 75 * @param uniqueStringElement the descendant or self that contains the 76 * unique String 77 * @return the unique String 78 */ getUniqueString(UiElement uniqueStringElement)79 protected abstract String getUniqueString(UiElement uniqueStringElement); 80 getUniqueStringFromSentinel(UiElement sentinel)81 private String getUniqueStringFromSentinel(UiElement sentinel) { 82 try { 83 return getUniqueString(uniqueStringFinder.find(sentinel)); 84 } catch (ElementNotFoundException e) { 85 return null; 86 } 87 } 88 89 @Override isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel)90 public boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel) { 91 // If the sentinel moved, scrolling has some effect. This is both an 92 // optimization - getBounds is cheaper than find - and necessary in 93 // certain cases, e.g. user is looking for a sibling of the unique string; 94 // the scroll is close to the end therefore the unique string does not 95 // change, but the target could be revealed. 96 if (!newSentinel.getBounds().equals(oldSentinel.getBounds())) { 97 return true; 98 } 99 100 String newString = getUniqueStringFromSentinel(newSentinel); 101 // A legitimate case for newString being null is when newSentinel is 102 // partially shown. We return true to allow further scrolling. But program 103 // error could also cause this, e.g. a bad choice of Getter, which 104 // results in unnecessary scroll actions that have no visual effect. This 105 // log helps troubleshooting in the latter case. 106 if (newString == null) { 107 Logs.logfmt(Log.WARN, "Unique String is null: sentinel=%s, uniqueStringFinder=%s", 108 newSentinel, uniqueStringFinder); 109 return true; 110 } 111 if (newString.equals(getUniqueStringFromSentinel(oldSentinel))) { 112 Logs.log(Log.INFO, "Unique String is not updated: " + newString); 113 return false; 114 } 115 return true; 116 } 117 118 @Override toString()119 public String toString() { 120 return Strings.toStringHelper(this).addValue(uniqueStringFinder).toString(); 121 } 122 } 123 124 /** 125 * Determines whether the sentinel is updated by checking the text of a 126 * descendant element of the sentinel (or itself). 127 */ 128 public static class TextUpdated extends SingleStringUpdated { TextUpdated(Finder uniqueStringFinder)129 public TextUpdated(Finder uniqueStringFinder) { 130 super(uniqueStringFinder); 131 } 132 133 @Override getUniqueString(UiElement uniqueStringElement)134 protected String getUniqueString(UiElement uniqueStringElement) { 135 return uniqueStringElement.getText(); 136 } 137 } 138 139 /** 140 * Determines whether the sentinel is updated by checking the content 141 * description of a descendant element of the sentinel (or itself). 142 */ 143 public static class ContentDescriptionUpdated extends SingleStringUpdated { ContentDescriptionUpdated(Finder uniqueStringFinder)144 public ContentDescriptionUpdated(Finder uniqueStringFinder) { 145 super(uniqueStringFinder); 146 } 147 148 @Override getUniqueString(UiElement uniqueStringElement)149 protected String getUniqueString(UiElement uniqueStringElement) { 150 return uniqueStringElement.getContentDescription(); 151 } 152 } 153 154 /** 155 * Determines whether the sentinel is updated by checking the resource-id of a 156 * descendant element of the sentinel (often itself). This is useful when the 157 * children of the container are heterogeneous -- they don't have a common 158 * pattern to get a unique string. 159 */ 160 public static class ResourceIdUpdated extends SingleStringUpdated { 161 /** 162 * Uses the resource-id of the sentinel itself. 163 */ 164 public static final ResourceIdUpdated SELF = new ResourceIdUpdated(By.any()); 165 ResourceIdUpdated(Finder uniqueStringFinder)166 public ResourceIdUpdated(Finder uniqueStringFinder) { 167 super(uniqueStringFinder); 168 } 169 170 @Override getUniqueString(UiElement uniqueStringElement)171 protected String getUniqueString(UiElement uniqueStringElement) { 172 return uniqueStringElement.getResourceId(); 173 } 174 } 175 176 private final IsUpdatedStrategy isUpdatedStrategy; 177 private UiElement lastSentinel; 178 179 /** 180 * Constructs with {@code Getter}s that decorate the given {@code Getter}s 181 * with {@link UiElement#VISIBLE}, and the given {@code isUpdatedStrategy} and 182 * {@code directionConverter}. Be careful with {@code Getter}s: the sentinel 183 * after each scroll should be unique. 184 */ DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter, Getter forwardGetter, DirectionConverter directionConverter)185 public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter, 186 Getter forwardGetter, DirectionConverter directionConverter) { 187 super(new MorePredicateGetter(backwardGetter, UiElement.VISIBLE), new MorePredicateGetter( 188 forwardGetter, UiElement.VISIBLE), directionConverter); 189 this.isUpdatedStrategy = isUpdatedStrategy; 190 } 191 192 /** 193 * Defaults to the standard {@link DirectionConverter}. 194 */ DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter, Getter forwardGetter)195 public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter, 196 Getter forwardGetter) { 197 this(isUpdatedStrategy, backwardGetter, forwardGetter, DirectionConverter.STANDARD_CONVERTER); 198 } 199 200 /** 201 * Defaults to LAST_CHILD_GETTER for forward scrolling, and the standard 202 * {@link DirectionConverter}. 203 */ DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter)204 public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter) { 205 this(isUpdatedStrategy, backwardGetter, LAST_CHILD_GETTER, 206 DirectionConverter.STANDARD_CONVERTER); 207 } 208 209 @Override scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction)210 public boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction) { 211 UiElement oldSentinel = getOldSentinel(driver, containerFinder, direction); 212 doScroll(oldSentinel.getParent(), direction); 213 UiElement newSentinel = getSentinel(driver, containerFinder, direction); 214 lastSentinel = newSentinel; 215 return isUpdatedStrategy.isSentinelUpdated(newSentinel, oldSentinel); 216 } 217 getOldSentinel(DroidDriver driver, Finder containerFinder, PhysicalDirection direction)218 private UiElement getOldSentinel(DroidDriver driver, Finder containerFinder, 219 PhysicalDirection direction) { 220 return lastSentinel != null ? lastSentinel : getSentinel(driver, containerFinder, direction); 221 } 222 223 @Override beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, PhysicalDirection direction)224 public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, 225 PhysicalDirection direction) { 226 lastSentinel = null; 227 } 228 229 @Override endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, PhysicalDirection direction)230 public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder, 231 PhysicalDirection direction) { 232 // Prevent memory leak 233 lastSentinel = null; 234 } 235 236 @Override toString()237 public String toString() { 238 return String.format("DynamicSentinelStrategy{%s, isUpdatedStrategy=%s}", super.toString(), 239 isUpdatedStrategy); 240 } 241 } 242