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