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