1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.O;
4 import static android.os.Build.VERSION_CODES.O_MR1;
5 import static org.robolectric.RuntimeEnvironment.getApiLevel;
6 import static org.robolectric.util.reflector.Reflector.reflector;
7 
8 import android.accessibilityservice.AccessibilityServiceInfo;
9 import android.content.Context;
10 import android.content.pm.ServiceInfo;
11 import android.graphics.Matrix;
12 import android.os.Handler;
13 import android.os.Looper;
14 import android.os.Message;
15 import android.util.ArrayMap;
16 import android.util.Log;
17 import android.view.MagnificationSpec;
18 import android.view.accessibility.AccessibilityEvent;
19 import android.view.accessibility.AccessibilityManager;
20 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
21 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
22 import android.view.accessibility.IAccessibilityManager;
23 import android.view.accessibility.IAccessibilityManager.WindowTransformationSpec;
24 import com.google.common.base.Preconditions;
25 import com.google.common.collect.ImmutableList;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.concurrent.CopyOnWriteArrayList;
31 import javax.annotation.Nullable;
32 import org.robolectric.annotation.ClassName;
33 import org.robolectric.annotation.HiddenApi;
34 import org.robolectric.annotation.Implementation;
35 import org.robolectric.annotation.Implements;
36 import org.robolectric.annotation.RealObject;
37 import org.robolectric.annotation.Resetter;
38 import org.robolectric.shadow.api.Shadow;
39 import org.robolectric.util.ReflectionHelpers;
40 import org.robolectric.util.ReflectionHelpers.ClassParameter;
41 import org.robolectric.util.reflector.Accessor;
42 import org.robolectric.util.reflector.Direct;
43 import org.robolectric.util.reflector.ForType;
44 import org.robolectric.versioning.AndroidVersions.U;
45 
46 @Implements(AccessibilityManager.class)
47 public class ShadowAccessibilityManager {
48   private static AccessibilityManager sInstance;
49   private static final Object sInstanceSync = new Object();
50 
51   @RealObject AccessibilityManager realAccessibilityManager;
52   private static final List<AccessibilityEvent> sentAccessibilityEvents = new ArrayList<>();
53   private static boolean enabled;
54   private static List<AccessibilityServiceInfo> installedAccessibilityServiceList =
55       new ArrayList<>();
56   private static List<AccessibilityServiceInfo> enabledAccessibilityServiceList = new ArrayList<>();
57   private static List<ServiceInfo> accessibilityServiceList = new ArrayList<>();
58   private static final HashMap<AccessibilityStateChangeListener, Handler>
59       onAccessibilityStateChangeListeners = new HashMap<>();
60   private static boolean touchExplorationEnabled;
61 
62   private static boolean isAccessibilityButtonSupported = true;
63 
64   @Resetter
reset()65   public static void reset() {
66     synchronized (sInstanceSync) {
67       sInstance = null;
68     }
69     sentAccessibilityEvents.clear();
70     enabled = false;
71     installedAccessibilityServiceList.clear();
72     enabledAccessibilityServiceList.clear();
73     accessibilityServiceList.clear();
74     onAccessibilityStateChangeListeners.clear();
75     touchExplorationEnabled = false;
76     isAccessibilityButtonSupported = true;
77   }
78 
79   @HiddenApi
80   @Implementation
getInstance(Context context)81   public static AccessibilityManager getInstance(Context context) throws Exception {
82     synchronized (sInstanceSync) {
83       if (sInstance == null) {
84         sInstance = createInstance(context);
85       }
86     }
87     return sInstance;
88   }
89 
createInstance(Context context)90   private static AccessibilityManager createInstance(Context context) {
91     AccessibilityManager accessibilityManager =
92         Shadow.newInstance(
93             AccessibilityManager.class,
94             new Class[] {Context.class, IAccessibilityManager.class, int.class},
95             new Object[] {
96               context, ReflectionHelpers.createNullProxy(IAccessibilityManager.class), 0
97             });
98     ReflectionHelpers.setField(
99         accessibilityManager,
100         "mHandler",
101         new MyHandler(context.getMainLooper(), accessibilityManager));
102     return accessibilityManager;
103   }
104 
105   @Implementation
addAccessibilityStateChangeListener(AccessibilityStateChangeListener listener)106   protected boolean addAccessibilityStateChangeListener(AccessibilityStateChangeListener listener) {
107     addAccessibilityStateChangeListener(listener, null);
108     return true;
109   }
110 
111   @Implementation(minSdk = O)
addAccessibilityStateChangeListener( AccessibilityStateChangeListener listener, Handler handler)112   protected void addAccessibilityStateChangeListener(
113       AccessibilityStateChangeListener listener, Handler handler) {
114     onAccessibilityStateChangeListeners.put(listener, handler);
115   }
116 
117   @Implementation
removeAccessibilityStateChangeListener( AccessibilityStateChangeListener listener)118   protected boolean removeAccessibilityStateChangeListener(
119       AccessibilityStateChangeListener listener) {
120     final boolean wasRegistered = onAccessibilityStateChangeListeners.containsKey(listener);
121     onAccessibilityStateChangeListeners.remove(listener);
122     return wasRegistered;
123   }
124 
125   @Implementation
getAccessibilityServiceList()126   protected List<ServiceInfo> getAccessibilityServiceList() {
127     return Collections.unmodifiableList(accessibilityServiceList);
128   }
129 
setInteractiveUiTimeout(int interactiveUiTimeoutMillis)130   public void setInteractiveUiTimeout(int interactiveUiTimeoutMillis) {
131     ReflectionHelpers.setField(
132         realAccessibilityManager, "mInteractiveUiTimeout", interactiveUiTimeoutMillis);
133   }
134 
setNonInteractiveUiTimeout(int nonInteractiveUiTimeoutMillis)135   public void setNonInteractiveUiTimeout(int nonInteractiveUiTimeoutMillis) {
136     ReflectionHelpers.setField(
137         realAccessibilityManager, "mNonInteractiveUiTimeout", nonInteractiveUiTimeoutMillis);
138   }
139 
setAccessibilityServiceList(List<ServiceInfo> accessibilityServiceList)140   public void setAccessibilityServiceList(List<ServiceInfo> accessibilityServiceList) {
141     Preconditions.checkNotNull(accessibilityServiceList);
142     this.accessibilityServiceList = new ArrayList<>(accessibilityServiceList);
143   }
144 
145   @Nullable
146   @Implementation
getEnabledAccessibilityServiceList( int feedbackTypeFlags)147   protected List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
148       int feedbackTypeFlags) {
149     return Collections.unmodifiableList(enabledAccessibilityServiceList);
150   }
151 
setEnabledAccessibilityServiceList( List<AccessibilityServiceInfo> enabledAccessibilityServiceList)152   public void setEnabledAccessibilityServiceList(
153       List<AccessibilityServiceInfo> enabledAccessibilityServiceList) {
154     Preconditions.checkNotNull(enabledAccessibilityServiceList);
155     this.enabledAccessibilityServiceList = new ArrayList<>(enabledAccessibilityServiceList);
156   }
157 
158   @Implementation
getInstalledAccessibilityServiceList()159   protected List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() {
160     return Collections.unmodifiableList(installedAccessibilityServiceList);
161   }
162 
setInstalledAccessibilityServiceList( List<AccessibilityServiceInfo> installedAccessibilityServiceList)163   public void setInstalledAccessibilityServiceList(
164       List<AccessibilityServiceInfo> installedAccessibilityServiceList) {
165     Preconditions.checkNotNull(installedAccessibilityServiceList);
166     this.installedAccessibilityServiceList = new ArrayList<>(installedAccessibilityServiceList);
167   }
168 
169   @Implementation
sendAccessibilityEvent(AccessibilityEvent event)170   protected void sendAccessibilityEvent(AccessibilityEvent event) {
171     sentAccessibilityEvents.add(event);
172     reflector(AccessibilityManagerReflector.class, realAccessibilityManager)
173         .sendAccessibilityEvent(event);
174   }
175 
176   /**
177    * Returns a list of all {@linkplain AccessibilityEvent accessibility events} that have been sent
178    * via {@link #sendAccessibilityEvent}.
179    */
getSentAccessibilityEvents()180   public ImmutableList<AccessibilityEvent> getSentAccessibilityEvents() {
181     return ImmutableList.copyOf(sentAccessibilityEvents);
182   }
183 
184   @Implementation
isEnabled()185   protected boolean isEnabled() {
186     return enabled;
187   }
188 
setEnabled(boolean enabled)189   public void setEnabled(boolean enabled) {
190     this.enabled = enabled;
191     ReflectionHelpers.setField(realAccessibilityManager, "mIsEnabled", enabled);
192     for (AccessibilityStateChangeListener l : onAccessibilityStateChangeListeners.keySet()) {
193       if (l != null) {
194         l.onAccessibilityStateChanged(enabled);
195       }
196     }
197   }
198 
199   @Implementation
isTouchExplorationEnabled()200   protected boolean isTouchExplorationEnabled() {
201     return touchExplorationEnabled;
202   }
203 
setTouchExplorationEnabled(boolean touchExplorationEnabled)204   public void setTouchExplorationEnabled(boolean touchExplorationEnabled) {
205     this.touchExplorationEnabled = touchExplorationEnabled;
206     List<TouchExplorationStateChangeListener> listeners = new ArrayList<>();
207     if (getApiLevel() >= O) {
208       listeners =
209           new ArrayList<>(
210               reflector(AccessibilityManagerReflector.class, realAccessibilityManager)
211                   .getTouchExplorationStateChangeListeners()
212                   .keySet());
213     } else {
214       listeners =
215           new ArrayList<>(
216               reflector(AccessibilityManagerReflectorN.class, realAccessibilityManager)
217                   .getTouchExplorationStateChangeListeners());
218     }
219     listeners.forEach(listener -> listener.onTouchExplorationStateChanged(touchExplorationEnabled));
220   }
221 
222   /**
223    * Returns {@code true} by default, or the value specified via {@link
224    * #setAccessibilityButtonSupported(boolean)}.
225    */
226   @Implementation(minSdk = O_MR1)
isAccessibilityButtonSupported()227   protected static boolean isAccessibilityButtonSupported() {
228     return isAccessibilityButtonSupported;
229   }
230 
231   @HiddenApi
232   @Implementation(minSdk = O)
performAccessibilityShortcut()233   protected void performAccessibilityShortcut() {
234     setEnabled(true);
235     setTouchExplorationEnabled(true);
236   }
237 
238   /**
239    * This shadow method is required because {@link
240    * android.view.accessibility.DirectAccessibilityConnection} calls it to determine if any
241    * transformations have occurred on this window.
242    */
243   @Implementation(minSdk = U.SDK_INT)
244   protected @ClassName("android.view.accessibility.IAccessibilityManager.WindowTransformationSpec")
getWindowTransformationSpec(int windowId)245   Object getWindowTransformationSpec(int windowId) {
246     // Return a value that represents no transformation.
247     WindowTransformationSpec spec = new WindowTransformationSpec();
248     spec.magnificationSpec = new MagnificationSpec();
249     float[] matrix = new float[9];
250     Matrix.IDENTITY_MATRIX.getValues(matrix);
251     spec.transformationMatrix = matrix;
252     return spec;
253   }
254 
255   /**
256    * Sets that the system navigation area is supported accessibility button; controls the return
257    * value of {@link AccessibilityManager#isAccessibilityButtonSupported()}.
258    */
setAccessibilityButtonSupported(boolean supported)259   public static void setAccessibilityButtonSupported(boolean supported) {
260     isAccessibilityButtonSupported = supported;
261   }
262 
263   static class MyHandler extends Handler {
264     private static final int DO_SET_STATE = 10;
265     private final AccessibilityManager accessibilityManager;
266 
MyHandler(Looper mainLooper, AccessibilityManager accessibilityManager)267     MyHandler(Looper mainLooper, AccessibilityManager accessibilityManager) {
268       super(mainLooper);
269       this.accessibilityManager = accessibilityManager;
270     }
271 
272     @Override
handleMessage(Message message)273     public void handleMessage(Message message) {
274       switch (message.what) {
275         case DO_SET_STATE:
276           ReflectionHelpers.callInstanceMethod(
277               accessibilityManager, "setState", ClassParameter.from(int.class, message.arg1));
278           return;
279         default:
280           Log.w("AccessibilityManager", "Unknown message type: " + message.what);
281       }
282     }
283   }
284 
285   @ForType(AccessibilityManager.class)
286   interface AccessibilityManagerReflector {
287 
288     @Direct
sendAccessibilityEvent(AccessibilityEvent event)289     void sendAccessibilityEvent(AccessibilityEvent event);
290 
291     @Accessor("mTouchExplorationStateChangeListeners")
292     ArrayMap<TouchExplorationStateChangeListener, Handler>
getTouchExplorationStateChangeListeners()293         getTouchExplorationStateChangeListeners();
294   }
295 
296   @ForType(AccessibilityManager.class)
297   interface AccessibilityManagerReflectorN {
298     @Accessor("mTouchExplorationStateChangeListeners")
299     CopyOnWriteArrayList<TouchExplorationStateChangeListener>
getTouchExplorationStateChangeListeners()300         getTouchExplorationStateChangeListeners();
301   }
302 }
303