aboutsummaryrefslogtreecommitdiff
path: root/src/io/appium/droiddriver/scroll/DynamicSentinelStrategy.java
blob: 051cfa7b37a0649cfb287ad96e811ff7028d700d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/*
 * Copyright (C) 2013 DroidDriver committers
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.appium.droiddriver.scroll;

import android.util.Log;

import io.appium.droiddriver.DroidDriver;
import io.appium.droiddriver.UiElement;
import io.appium.droiddriver.exceptions.ElementNotFoundException;
import io.appium.droiddriver.finders.By;
import io.appium.droiddriver.finders.Finder;
import io.appium.droiddriver.scroll.Direction.DirectionConverter;
import io.appium.droiddriver.scroll.Direction.PhysicalDirection;
import io.appium.droiddriver.util.Logs;
import io.appium.droiddriver.util.Strings;

/**
 * Determines whether scrolling is possible by checking whether the sentinel
 * child is updated after scrolling. Use this when {@link UiElement#getChildren}
 * is not reliable. This can happen, for instance, when UiAutomationDriver is
 * used, which skips invisible children, or in the case of dynamic list, which
 * shows more items when scrolling beyond the end.
 */
public class DynamicSentinelStrategy extends SentinelStrategy {

  /**
   * Interface for determining whether sentinel is updated.
   */
  public static interface IsUpdatedStrategy {
    /**
     * Returns whether {@code newSentinel} is updated from {@code oldSentinel}.
     */
    boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel);

    /**
     * {@inheritDoc}
     *
     * <p>
     * It is recommended that this method return a description to help
     * debugging.
     */
    @Override
    String toString();
  }

  /**
   * Determines whether the sentinel is updated by checking a single unique
   * String attribute of a descendant element of the sentinel (or itself).
   */
  public static abstract class SingleStringUpdated implements IsUpdatedStrategy {
    private final Finder uniqueStringFinder;

    /**
     * @param uniqueStringFinder a Finder relative to the sentinel that finds
     *        its descendant or self which contains a unique String.
     */
    public SingleStringUpdated(Finder uniqueStringFinder) {
      this.uniqueStringFinder = uniqueStringFinder;
    }

    /**
     * @param uniqueStringElement the descendant or self that contains the
     *        unique String
     * @return the unique String
     */
    protected abstract String getUniqueString(UiElement uniqueStringElement);

    private String getUniqueStringFromSentinel(UiElement sentinel) {
      try {
        return getUniqueString(uniqueStringFinder.find(sentinel));
      } catch (ElementNotFoundException e) {
        return null;
      }
    }

    @Override
    public boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel) {
      // If the sentinel moved, scrolling has some effect. This is both an
      // optimization - getBounds is cheaper than find - and necessary in
      // certain cases, e.g. user is looking for a sibling of the unique string;
      // the scroll is close to the end therefore the unique string does not
      // change, but the target could be revealed.
      if (!newSentinel.getBounds().equals(oldSentinel.getBounds())) {
        return true;
      }

      String newString = getUniqueStringFromSentinel(newSentinel);
      // A legitimate case for newString being null is when newSentinel is
      // partially shown. We return true to allow further scrolling. But program
      // error could also cause this, e.g. a bad choice of Getter, which
      // results in unnecessary scroll actions that have no visual effect. This
      // log helps troubleshooting in the latter case.
      if (newString == null) {
        Logs.logfmt(Log.WARN, "Unique String is null: sentinel=%s, uniqueStringFinder=%s",
            newSentinel, uniqueStringFinder);
        return true;
      }
      if (newString.equals(getUniqueStringFromSentinel(oldSentinel))) {
        Logs.log(Log.INFO, "Unique String is not updated: " + newString);
        return false;
      }
      return true;
    }

    @Override
    public String toString() {
      return Strings.toStringHelper(this).addValue(uniqueStringFinder).toString();
    }
  }

  /**
   * Determines whether the sentinel is updated by checking the text of a
   * descendant element of the sentinel (or itself).
   */
  public static class TextUpdated extends SingleStringUpdated {
    public TextUpdated(Finder uniqueStringFinder) {
      super(uniqueStringFinder);
    }

    @Override
    protected String getUniqueString(UiElement uniqueStringElement) {
      return uniqueStringElement.getText();
    }
  }

  /**
   * Determines whether the sentinel is updated by checking the content
   * description of a descendant element of the sentinel (or itself).
   */
  public static class ContentDescriptionUpdated extends SingleStringUpdated {
    public ContentDescriptionUpdated(Finder uniqueStringFinder) {
      super(uniqueStringFinder);
    }

    @Override
    protected String getUniqueString(UiElement uniqueStringElement) {
      return uniqueStringElement.getContentDescription();
    }
  }

  /**
   * Determines whether the sentinel is updated by checking the resource-id of a
   * descendant element of the sentinel (often itself). This is useful when the
   * children of the container are heterogeneous -- they don't have a common
   * pattern to get a unique string.
   */
  public static class ResourceIdUpdated extends SingleStringUpdated {
    /**
     * Uses the resource-id of the sentinel itself.
     */
    public static final ResourceIdUpdated SELF = new ResourceIdUpdated(By.any());

    public ResourceIdUpdated(Finder uniqueStringFinder) {
      super(uniqueStringFinder);
    }

    @Override
    protected String getUniqueString(UiElement uniqueStringElement) {
      return uniqueStringElement.getResourceId();
    }
  }

  private final IsUpdatedStrategy isUpdatedStrategy;
  private UiElement lastSentinel;

  /**
   * Constructs with {@code Getter}s that decorate the given {@code Getter}s
   * with {@link UiElement#VISIBLE}, and the given {@code isUpdatedStrategy} and
   * {@code directionConverter}. Be careful with {@code Getter}s: the sentinel
   * after each scroll should be unique.
   */
  public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter,
      Getter forwardGetter, DirectionConverter directionConverter) {
    super(new MorePredicateGetter(backwardGetter, UiElement.VISIBLE), new MorePredicateGetter(
        forwardGetter, UiElement.VISIBLE), directionConverter);
    this.isUpdatedStrategy = isUpdatedStrategy;
  }

  /**
   * Defaults to the standard {@link DirectionConverter}.
   */
  public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter,
      Getter forwardGetter) {
    this(isUpdatedStrategy, backwardGetter, forwardGetter, DirectionConverter.STANDARD_CONVERTER);
  }

  /**
   * Defaults to LAST_CHILD_GETTER for forward scrolling, and the standard
   * {@link DirectionConverter}.
   */
  public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter) {
    this(isUpdatedStrategy, backwardGetter, LAST_CHILD_GETTER,
        DirectionConverter.STANDARD_CONVERTER);
  }

  @Override
  public boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction) {
    UiElement oldSentinel = getOldSentinel(driver, containerFinder, direction);
    doScroll(oldSentinel.getParent(), direction);
    UiElement newSentinel = getSentinel(driver, containerFinder, direction);
    lastSentinel = newSentinel;
    return isUpdatedStrategy.isSentinelUpdated(newSentinel, oldSentinel);
  }

  private UiElement getOldSentinel(DroidDriver driver, Finder containerFinder,
      PhysicalDirection direction) {
    return lastSentinel != null ? lastSentinel : getSentinel(driver, containerFinder, direction);
  }

  @Override
  public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
      PhysicalDirection direction) {
    lastSentinel = null;
  }

  @Override
  public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
      PhysicalDirection direction) {
    // Prevent memory leak
    lastSentinel = null;
  }

  @Override
  public String toString() {
    return String.format("DynamicSentinelStrategy{%s, isUpdatedStrategy=%s}", super.toString(),
        isUpdatedStrategy);
  }
}