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