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 */
16package com.google.android.droiddriver.scroll;
17
18import android.util.Log;
19
20import com.google.android.droiddriver.DroidDriver;
21import com.google.android.droiddriver.UiElement;
22import com.google.android.droiddriver.exceptions.ElementNotFoundException;
23import com.google.android.droiddriver.finders.By;
24import com.google.android.droiddriver.finders.Finder;
25import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
26import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
27import com.google.android.droiddriver.util.Logs;
28import com.google.android.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 */
37public 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     */
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 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     */
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     */
79    protected abstract String getUniqueString(UiElement uniqueStringElement);
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      // 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
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 {
129    public TextUpdated(Finder uniqueStringFinder) {
130      super(uniqueStringFinder);
131    }
132
133    @Override
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 {
144    public ContentDescriptionUpdated(Finder uniqueStringFinder) {
145      super(uniqueStringFinder);
146    }
147
148    @Override
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
166    public ResourceIdUpdated(Finder uniqueStringFinder) {
167      super(uniqueStringFinder);
168    }
169
170    @Override
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   */
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   */
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   */
204  public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter) {
205    this(isUpdatedStrategy, backwardGetter, LAST_CHILD_GETTER,
206        DirectionConverter.STANDARD_CONVERTER);
207  }
208
209  @Override
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
218  private UiElement getOldSentinel(DroidDriver driver, Finder containerFinder,
219      PhysicalDirection direction) {
220    return lastSentinel != null ? lastSentinel : getSentinel(driver, containerFinder, direction);
221  }
222
223  @Override
224  public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
225      PhysicalDirection direction) {
226    lastSentinel = null;
227  }
228
229  @Override
230  public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
231      PhysicalDirection direction) {
232    // Prevent memory leak
233    lastSentinel = null;
234  }
235
236  @Override
237  public String toString() {
238    return String.format("DynamicSentinelStrategy{%s, isUpdatedStrategy=%s}", super.toString(),
239        isUpdatedStrategy);
240  }
241}
242