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.finders.Predicate;
26import com.google.android.droiddriver.finders.Predicates;
27import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
28import com.google.android.droiddriver.scroll.Direction.LogicalDirection;
29import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
30import com.google.android.droiddriver.util.Logs;
31
32import java.util.List;
33
34/**
35 * A {@link ScrollStepStrategy} that determines whether scrolling is possible
36 * based on a sentinel.
37 */
38public 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
46    protected Getter() {
47      // Include invisible children by default.
48      this(null);
49    }
50
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
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
71    protected abstract UiElement getSentinel(List<? extends UiElement> children);
72
73    @Override
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
148    public MorePredicateGetter(Getter original, Predicate<? super UiElement> extraPredicate) {
149      super(Predicates.allOf(original.predicate, extraPredicate));
150      this.original = original;
151    }
152
153    @Override
154    protected UiElement getSentinel(List<? extends UiElement> children) {
155      return original.getSentinel(children);
156    }
157
158    @Override
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
168  protected SentinelStrategy(Getter backwardGetter, Getter forwardGetter,
169      DirectionConverter directionConverter) {
170    this.backwardGetter = backwardGetter;
171    this.forwardGetter = forwardGetter;
172    this.directionConverter = directionConverter;
173  }
174
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
189  public final DirectionConverter getDirectionConverter() {
190    return directionConverter;
191  }
192
193  @Override
194  public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
195      PhysicalDirection direction) {}
196
197  @Override
198  public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
199      PhysicalDirection direction) {}
200
201  @Override
202  public String toString() {
203    return String.format("{backwardGetter=%s, forwardGetter=%s}", backwardGetter, forwardGetter);
204  }
205
206  @Override
207  public void doScroll(UiElement container, PhysicalDirection direction) {
208    container.scroll(direction);
209  }
210}
211