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