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 com.google.android.droiddriver.scroll; 17 18 import android.util.Log; 19 20 import com.google.android.droiddriver.DroidDriver; 21 import com.google.android.droiddriver.UiElement; 22 import com.google.android.droiddriver.actions.ScrollDirection; 23 import com.google.android.droiddriver.exceptions.ElementNotFoundException; 24 import com.google.android.droiddriver.finders.Finder; 25 import com.google.android.droiddriver.scroll.Direction.PhysicalToLogicalConverter; 26 import com.google.android.droiddriver.util.Logs; 27 import com.google.common.base.Objects; 28 29 /** 30 * Determines whether scrolling is possible by checking whether the sentinel 31 * child is updated after scrolling. Use this when 32 * {@link UiElement#getChildCount()} is not reliable. This can happen, for 33 * instance, when UiAutomationDriver is used, which skips invisible children, or 34 * in the case of dynamic list, which shows more items when scrolling beyond the 35 * end. 36 */ 37 public class DynamicSentinelStrategy extends AbstractSentinelStrategy { 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 */ 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 56 String toString(); 57 } 58 59 /** 60 * Determines whether the sentinel is updated by checking a single unique 61 * String attribute of a child 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 child element which contains a unique String. 69 */ 70 public SingleStringUpdated(Finder uniqueStringFinder) { 71 this.uniqueStringFinder = uniqueStringFinder; 72 } 73 74 /** 75 * @param uniqueStringChild the child of sentinel (or itself) that contains 76 * the unique String 77 * @return the unique String 78 */ 79 protected abstract String getUniqueString(UiElement uniqueStringChild); 80 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 90 public boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel) { 91 String newString = getUniqueStringFromSentinel(newSentinel); 92 // If newString is null, newSentinel must be partially shown. In this case 93 // we return true to allow further scrolling. But program error could also 94 // cause this, e.g. a bad choice of GetStrategy. log for debugging. 95 if (newString == null) { 96 Logs.logfmt(Log.WARN, "Unique String under sentinel %s is null", newSentinel); 97 return true; 98 } 99 if (newString.equals(getUniqueStringFromSentinel(oldSentinel))) { 100 Logs.log(Log.INFO, "Unique String is not updated: " + newString); 101 return false; 102 } 103 return true; 104 } 105 106 @Override 107 public String toString() { 108 return Objects.toStringHelper(this).addValue(uniqueStringFinder).toString(); 109 } 110 } 111 112 /** 113 * Determines whether the sentinel is updated by checking the text of a child 114 * element of the sentinel (or itself). 115 */ 116 public static class TextUpdated extends SingleStringUpdated { 117 public TextUpdated(Finder uniqueStringFinder) { 118 super(uniqueStringFinder); 119 } 120 121 @Override 122 protected String getUniqueString(UiElement uniqueStringChild) { 123 return uniqueStringChild.getText(); 124 } 125 } 126 127 /** 128 * Determines whether the sentinel is updated by checking the content 129 * description of a child element of the sentinel (or itself). 130 */ 131 public static class ContentDescriptionUpdated extends SingleStringUpdated { 132 public ContentDescriptionUpdated(Finder uniqueStringFinder) { 133 super(uniqueStringFinder); 134 } 135 136 @Override 137 protected String getUniqueString(UiElement uniqueStringChild) { 138 return uniqueStringChild.getContentDescription(); 139 } 140 } 141 142 private final IsUpdatedStrategy isUpdatedStrategy; 143 144 /** 145 * Constructs with {@code GetStrategy}s that decorate the given 146 * {@code GetStrategy}s with {@link UiElement#VISIBLE}, and the given 147 * {@code isUpdatedStrategy} and {@code physicalToLogicalConverter}. Be 148 * careful with {@code GetStrategy}s: the sentinel after each scroll should be 149 * unique. 150 */ 151 public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, 152 GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy, 153 PhysicalToLogicalConverter physicalToLogicalConverter) { 154 super(new MorePredicateGetStrategy(backwardGetStrategy, UiElement.VISIBLE, "VISIBLE_"), 155 new MorePredicateGetStrategy(forwardGetStrategy, UiElement.VISIBLE, "VISIBLE_"), 156 physicalToLogicalConverter); 157 this.isUpdatedStrategy = isUpdatedStrategy; 158 } 159 160 /** 161 * Defaults to the standard {@link PhysicalToLogicalConverter}. 162 */ 163 public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, 164 GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy) { 165 this(isUpdatedStrategy, backwardGetStrategy, forwardGetStrategy, 166 PhysicalToLogicalConverter.STANDARD_CONVERTER); 167 } 168 169 /** 170 * Defaults to LAST_CHILD_GETTER for forward scrolling, and the standard 171 * {@link PhysicalToLogicalConverter}. 172 */ 173 public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, 174 GetStrategy backwardGetStrategy) { 175 this(isUpdatedStrategy, backwardGetStrategy, LAST_CHILD_GETTER, 176 PhysicalToLogicalConverter.STANDARD_CONVERTER); 177 } 178 179 @Override 180 public boolean scroll(DroidDriver driver, Finder parentFinder, ScrollDirection direction) { 181 UiElement parent = driver.on(parentFinder); 182 UiElement oldSentinel = getSentinel(parent, direction); 183 parent.scroll(direction); 184 UiElement newSentinel = getSentinel(driver.on(parentFinder), direction); 185 return isUpdatedStrategy.isSentinelUpdated(newSentinel, oldSentinel); 186 } 187 188 @Override 189 public String toString() { 190 return String.format("DynamicSentinelStrategy{%s, isUpdatedStrategy=%s}", super.toString(), 191 isUpdatedStrategy); 192 } 193 } 194