blob: 35c350a30b510c72592894e85257d8b7ceb2f45e [file] [log] [blame]
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.O_MR1;
import static org.robolectric.RuntimeEnvironment.getApiLevel;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
import android.content.pm.ServiceInfo;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.ArrayMap;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
import android.view.accessibility.IAccessibilityManager;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.annotation.Nullable;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
@Implements(AccessibilityManager.class)
public class ShadowAccessibilityManager {
private static AccessibilityManager sInstance;
private static final Object sInstanceSync = new Object();
@RealObject AccessibilityManager realAccessibilityManager;
private final List<AccessibilityEvent> sentAccessibilityEvents = new ArrayList<>();
private boolean enabled;
private List<AccessibilityServiceInfo> installedAccessibilityServiceList = new ArrayList<>();
private List<AccessibilityServiceInfo> enabledAccessibilityServiceList = new ArrayList<>();
private List<ServiceInfo> accessibilityServiceList = new ArrayList<>();
private final HashMap<AccessibilityStateChangeListener, Handler>
onAccessibilityStateChangeListeners = new HashMap<>();
private boolean touchExplorationEnabled;
private static boolean isAccessibilityButtonSupported = true;
@Resetter
public static void reset() {
synchronized (sInstanceSync) {
sInstance = null;
}
isAccessibilityButtonSupported = true;
}
@HiddenApi
@Implementation
public static AccessibilityManager getInstance(Context context) throws Exception {
synchronized (sInstanceSync) {
if (sInstance == null) {
sInstance = createInstance(context);
}
}
return sInstance;
}
private static AccessibilityManager createInstance(Context context) {
if (getApiLevel() >= KITKAT) {
AccessibilityManager accessibilityManager =
Shadow.newInstance(
AccessibilityManager.class,
new Class[] {Context.class, IAccessibilityManager.class, int.class},
new Object[] {
context, ReflectionHelpers.createNullProxy(IAccessibilityManager.class), 0
});
ReflectionHelpers.setField(
accessibilityManager,
"mHandler",
new MyHandler(context.getMainLooper(), accessibilityManager));
return accessibilityManager;
} else {
AccessibilityManager accessibilityManager =
Shadow.newInstance(AccessibilityManager.class, new Class[0], new Object[0]);
ReflectionHelpers.setField(
accessibilityManager,
"mHandler",
new MyHandler(context.getMainLooper(), accessibilityManager));
ReflectionHelpers.setField(
accessibilityManager,
"mService",
ReflectionHelpers.createNullProxy(IAccessibilityManager.class));
return accessibilityManager;
}
}
@Implementation
protected boolean addAccessibilityStateChangeListener(AccessibilityStateChangeListener listener) {
addAccessibilityStateChangeListener(listener, null);
return true;
}
@Implementation(minSdk = O)
protected void addAccessibilityStateChangeListener(
AccessibilityStateChangeListener listener, Handler handler) {
onAccessibilityStateChangeListeners.put(listener, handler);
}
@Implementation
protected boolean removeAccessibilityStateChangeListener(
AccessibilityStateChangeListener listener) {
final boolean wasRegistered = onAccessibilityStateChangeListeners.containsKey(listener);
onAccessibilityStateChangeListeners.remove(listener);
return wasRegistered;
}
@Implementation
protected List<ServiceInfo> getAccessibilityServiceList() {
return Collections.unmodifiableList(accessibilityServiceList);
}
public void setAccessibilityServiceList(List<ServiceInfo> accessibilityServiceList) {
this.accessibilityServiceList = new ArrayList<>(accessibilityServiceList);
}
@Nullable
@Implementation
protected List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
int feedbackTypeFlags) {
// TODO(hoisie): prohibit null values for enabledAccessibilityServiceList
if (enabledAccessibilityServiceList == null) {
return null;
}
return Collections.unmodifiableList(enabledAccessibilityServiceList);
}
public void setEnabledAccessibilityServiceList(
List<AccessibilityServiceInfo> enabledAccessibilityServiceList) {
this.enabledAccessibilityServiceList =
enabledAccessibilityServiceList == null
? null
: new ArrayList<>(enabledAccessibilityServiceList);
}
@Implementation
protected List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() {
return Collections.unmodifiableList(installedAccessibilityServiceList);
}
public void setInstalledAccessibilityServiceList(
List<AccessibilityServiceInfo> installedAccessibilityServiceList) {
this.installedAccessibilityServiceList = new ArrayList<>(installedAccessibilityServiceList);
}
@Implementation
protected void sendAccessibilityEvent(AccessibilityEvent event) {
sentAccessibilityEvents.add(event);
reflector(AccessibilityManagerReflector.class, realAccessibilityManager)
.sendAccessibilityEvent(event);
}
/**
* Returns a list of all {@linkplain AccessibilityEvent accessibility events} that have been sent
* via {@link #sendAccessibilityEvent}.
*/
public ImmutableList<AccessibilityEvent> getSentAccessibilityEvents() {
return ImmutableList.copyOf(sentAccessibilityEvents);
}
@Implementation
protected boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
ReflectionHelpers.setField(realAccessibilityManager, "mIsEnabled", enabled);
for (AccessibilityStateChangeListener l : onAccessibilityStateChangeListeners.keySet()) {
if (l != null) {
l.onAccessibilityStateChanged(enabled);
}
}
}
@Implementation
protected boolean isTouchExplorationEnabled() {
return touchExplorationEnabled;
}
public void setTouchExplorationEnabled(boolean touchExplorationEnabled) {
this.touchExplorationEnabled = touchExplorationEnabled;
List<TouchExplorationStateChangeListener> listeners = new ArrayList<>();
if (getApiLevel() >= O) {
listeners =
new ArrayList<>(
reflector(AccessibilityManagerReflector.class, realAccessibilityManager)
.getTouchExplorationStateChangeListeners()
.keySet());
} else if (getApiLevel() >= KITKAT) {
listeners =
new ArrayList<>(
reflector(AccessibilityManagerReflectorN.class, realAccessibilityManager)
.getTouchExplorationStateChangeListeners());
}
listeners.forEach(listener -> listener.onTouchExplorationStateChanged(touchExplorationEnabled));
}
/**
* Returns {@code true} by default, or the value specified via {@link
* #setAccessibilityButtonSupported(boolean)}.
*/
@Implementation(minSdk = O_MR1)
protected static boolean isAccessibilityButtonSupported() {
return isAccessibilityButtonSupported;
}
@HiddenApi
@Implementation(minSdk = O)
protected void performAccessibilityShortcut() {
setEnabled(true);
setTouchExplorationEnabled(true);
}
/**
* Sets that the system navigation area is supported accessibility button; controls the return
* value of {@link AccessibilityManager#isAccessibilityButtonSupported()}.
*/
public static void setAccessibilityButtonSupported(boolean supported) {
isAccessibilityButtonSupported = supported;
}
static class MyHandler extends Handler {
private static final int DO_SET_STATE = 10;
private final AccessibilityManager accessibilityManager;
MyHandler(Looper mainLooper, AccessibilityManager accessibilityManager) {
super(mainLooper);
this.accessibilityManager = accessibilityManager;
}
@Override
public void handleMessage(Message message) {
switch (message.what) {
case DO_SET_STATE:
ReflectionHelpers.callInstanceMethod(
accessibilityManager, "setState", ClassParameter.from(int.class, message.arg1));
return;
default:
Log.w("AccessibilityManager", "Unknown message type: " + message.what);
}
}
}
@ForType(AccessibilityManager.class)
interface AccessibilityManagerReflector {
@Direct
void sendAccessibilityEvent(AccessibilityEvent event);
@Accessor("mTouchExplorationStateChangeListeners")
ArrayMap<TouchExplorationStateChangeListener, Handler>
getTouchExplorationStateChangeListeners();
}
@ForType(AccessibilityManager.class)
interface AccessibilityManagerReflectorN {
@Accessor("mTouchExplorationStateChangeListeners")
CopyOnWriteArrayList<TouchExplorationStateChangeListener>
getTouchExplorationStateChangeListeners();
}
}