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
17package io.appium.droiddriver.uiautomation;
18
19import android.annotation.TargetApi;
20import android.app.Instrumentation;
21import android.app.UiAutomation;
22import android.content.Context;
23import android.os.SystemClock;
24import android.view.accessibility.AccessibilityEvent;
25import android.view.accessibility.AccessibilityManager;
26import android.view.accessibility.AccessibilityNodeInfo;
27
28import io.appium.droiddriver.actions.InputInjector;
29import io.appium.droiddriver.base.BaseDroidDriver;
30import io.appium.droiddriver.exceptions.TimeoutException;
31import io.appium.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
32import io.appium.droiddriver.util.Logs;
33
34/**
35 * Implementation of DroidDriver that gets attributes via the Accessibility API
36 * and is acted upon via synthesized events.
37 */
38@TargetApi(18)
39public class UiAutomationDriver extends BaseDroidDriver<AccessibilityNodeInfo, UiAutomationElement> {
40  // This is a magic const copied from UiAutomator.
41  /**
42   * This value has the greatest bearing on the appearance of test execution
43   * speeds. This value is used as the minimum time to wait before considering
44   * the UI idle after each action.
45   */
46  private static final long QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE = 500;// ms
47  private static long idleTimeoutMillis = QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE;
48
49  /** Sets the {@code idleTimeoutMillis} argument for calling {@link UiAutomation#waitForIdle} */
50  public static void setIdleTimeoutMillis(long idleTimeoutMillis) {
51    UiAutomationDriver.idleTimeoutMillis = idleTimeoutMillis;
52  }
53
54  private final UiAutomationContext context;
55  private final InputInjector injector;
56  private final UiAutomationUiDevice uiDevice;
57  private AccessibilityNodeInfoCacheClearer clearer =
58      new WindowStateAccessibilityNodeInfoCacheClearer();
59
60  public UiAutomationDriver(Instrumentation instrumentation) {
61    context = new UiAutomationContext(instrumentation, this);
62    injector = new UiAutomationInputInjector(context);
63    uiDevice = new UiAutomationUiDevice(context);
64  }
65
66  @Override
67  public InputInjector getInjector() {
68    return injector;
69  }
70
71  @Override
72  protected UiAutomationElement newRootElement() {
73    return context.newRootElement(getRootNode());
74  }
75
76  @Override
77  protected UiAutomationElement newUiElement(AccessibilityNodeInfo rawElement,
78      UiAutomationElement parent) {
79    return new UiAutomationElement(context, rawElement, parent);
80  }
81
82  private AccessibilityNodeInfo getRootNode() {
83    final long timeoutMillis = getPoller().getTimeoutMillis();
84    context.callUiAutomation(new UiAutomationCallable<Void>() {
85      @Override
86      public Void call(UiAutomation uiAutomation) {
87        try {
88          uiAutomation.waitForIdle(idleTimeoutMillis, timeoutMillis);
89          return null;
90        } catch (java.util.concurrent.TimeoutException e) {
91          throw new TimeoutException(e);
92        }
93      }
94    });
95
96    long end = SystemClock.uptimeMillis() + timeoutMillis;
97    while (true) {
98      AccessibilityNodeInfo root =
99          context.callUiAutomation(new UiAutomationCallable<AccessibilityNodeInfo>() {
100            @Override
101            public AccessibilityNodeInfo call(UiAutomation uiAutomation) {
102              return uiAutomation.getRootInActiveWindow();
103            }
104          });
105      if (root != null) {
106        return root;
107      }
108      long remainingMillis = end - SystemClock.uptimeMillis();
109      if (remainingMillis < 0) {
110        throw new TimeoutException(
111            String.format("Timed out after %d milliseconds waiting for root AccessibilityNodeInfo",
112                timeoutMillis));
113      }
114      SystemClock.sleep(Math.min(250, remainingMillis));
115    }
116  }
117
118  /**
119   * Some widgets fail to trigger some AccessibilityEvent's after actions,
120   * resulting in stale AccessibilityNodeInfo's. As a work-around, force to
121   * clear the AccessibilityNodeInfoCache.
122   */
123  public void clearAccessibilityNodeInfoCache() {
124    Logs.call(this, "clearAccessibilityNodeInfoCache");
125    clearer.clearAccessibilityNodeInfoCache(this);
126  }
127
128  public interface AccessibilityNodeInfoCacheClearer {
129    void clearAccessibilityNodeInfoCache(UiAutomationDriver driver);
130  }
131
132  /**
133   * Clears AccessibilityNodeInfoCache by turning screen off then on.
134   */
135  public static class ScreenOffAccessibilityNodeInfoCacheClearer implements
136      AccessibilityNodeInfoCacheClearer {
137    public void clearAccessibilityNodeInfoCache(UiAutomationDriver driver) {
138      driver.getUiDevice().sleep();
139      driver.getUiDevice().wakeUp();
140    }
141  }
142
143  /**
144   * Clears AccessibilityNodeInfoCache by exploiting an implementation detail of
145   * AccessibilityNodeInfoCache. This is a hack; use it at your own discretion.
146   */
147  public static class WindowStateAccessibilityNodeInfoCacheClearer implements
148      AccessibilityNodeInfoCacheClearer {
149    public void clearAccessibilityNodeInfoCache(UiAutomationDriver driver) {
150      AccessibilityManager accessibilityManager =
151          (AccessibilityManager) driver.context.getInstrumentation().getTargetContext()
152              .getSystemService(Context.ACCESSIBILITY_SERVICE);
153      accessibilityManager.sendAccessibilityEvent(AccessibilityEvent
154          .obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED));
155    }
156  }
157
158  public void setAccessibilityNodeInfoCacheClearer(AccessibilityNodeInfoCacheClearer clearer) {
159    this.clearer = clearer;
160  }
161
162  @Override
163  public UiAutomationUiDevice getUiDevice() {
164    return uiDevice;
165  }
166}
167