blob: d92244ca5366f81b004335d1fe94ca5457a5a6fd [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.ui.accessibility;
import static android.accessibilityservice.AccessibilityServiceInfo.CAPABILITY_CAN_PERFORM_GESTURES;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_SPOKEN;
import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS;
import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.Activity;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.provider.Settings;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.autofill.AutofillManager;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
/**
* Provides utility methods relating to measuring accessibility state on Android. See native
* counterpart in ui::accessibility::AccessibilityState.
*/
@JNINamespace("ui")
public class AccessibilityState {
private static final String TAG = "A11yState";
public static final int EVENT_TYPE_MASK_ALL = ~0;
public static final int EVENT_TYPE_MASK_NONE = 0;
public static final String AUTOFILL_COMPAT_ACCESSIBILITY_SERVICE_ID =
"android/com.android.server.autofill.AutofillCompatAccessibilityService";
/**
* Interface for the observers of the system's accessibility state.
*/
public interface Listener {
/**
* Called when any aspect of the system's accessibility state changes. This can happen for
* example when a user:
* - enables/disables an accessibility service (e.g. TalkBack, VoiceAccess, etc.)
* - enables/disables a pseudo-accessibility service (e.g. password manager, etc.)
* - changes an accessibility-related system setting (e.g. animation duration, password
* obscuring, touch exploration, etc.)
*
* For a full list of triggers, see: {AccessibilityState#registerObservers}
* For a full list of tracked settings, see: {AccessibilityState.State}
*
* This method passes both the previous and new (old current vs. now current) accessibility
* state. Clients that are only interested in a subset of the state should compare the
* oldAccessibilityState to newAccessibilityState to see if a relevant setting changed.
*
* @param oldAccessibilityState The previous accessibility state
* @param newAccessibilityState The new accessibility state
*/
void onAccessibilityStateChanged(State oldAccessibilityState, State newAccessibilityState);
}
/** A representation of the current accessibility state. */
public static class State {
// True when we determine that genuine assistive technology such as a screen reader
// is running, based on the information from running accessibility services. False
// otherwise.
public final boolean isScreenReaderEnabled;
// True when the user has touch exploration enabled. False otherwise.
public final boolean isTouchExplorationEnabled;
// True when a service that requested to perform gestures is enabled. False otherwise.
public final boolean isPerformGesturesEnabled;
// True when at least one accessibility service is enabled. False otherwise.
public final boolean isAnyAccessibilityServiceEnabled;
// True when android version is less than 31 or at least one enabled accessibility service
// returns true for isAccessibilityTool(). False otherwise.
public final boolean isAccessibilityToolPresent;
// True when the user is running at least one service that requests the FEEDBACK_SPOKEN
// feedback type in AccessibilityServiceInfo. False otherwise.
public final boolean isSpokenFeedbackServicePresent;
// True when the user has enabled the Android-OS privacy setting for showing passwords,
// found in: Settings > Privacy > Show passwords. (Settings.System.TEXT_SHOW_PASSWORD).
// False otherwise.
public final boolean isTextShowPasswordEnabled;
// True when we suspect that only password managers are enabled, based on the information
// from running accessibility services. False otherwise.
public final boolean isOnlyPasswordManagersEnabled;
public State(boolean isScreenReaderEnabled, boolean isTouchExplorationEnabled,
boolean isPerformGesturesEnabled, boolean isAnyAccessibilityServiceEnabled,
boolean isAccessibilityToolPresent, boolean isSpokenFeedbackServicePresent,
boolean isTextShowPasswordEnabled, boolean isOnlyPasswordManagersEnabled) {
this.isScreenReaderEnabled = isScreenReaderEnabled;
this.isTouchExplorationEnabled = isTouchExplorationEnabled;
this.isPerformGesturesEnabled = isPerformGesturesEnabled;
this.isAnyAccessibilityServiceEnabled = isAnyAccessibilityServiceEnabled;
this.isAccessibilityToolPresent = isAccessibilityToolPresent;
this.isSpokenFeedbackServicePresent = isSpokenFeedbackServicePresent;
this.isTextShowPasswordEnabled = isTextShowPasswordEnabled;
this.isOnlyPasswordManagersEnabled = isOnlyPasswordManagersEnabled;
}
}
// Analysis of the most popular accessibility services on Android suggests
// that any service that requests any of these three events is a screen reader
// or other complete assistive technology. If none of these events are requested,
// we can enable some optimizations.
private static final int SCREEN_READER_EVENT_TYPE_MASK = AccessibilityEvent.TYPE_VIEW_SELECTED
| AccessibilityEvent.TYPE_VIEW_SCROLLED | AccessibilityEvent.TYPE_ANNOUNCEMENT;
// Analysis of the most popular password managers on Android suggests
// that services that only request these events, flags, and capabilities is likely a password
// manager. If not more than these events are requested, we can enable some optimizations.
private static final int PASSWORD_MANAGER_EVENT_TYPE_MASK = AccessibilityEvent.TYPE_VIEW_CLICKED
| AccessibilityEvent.TYPE_VIEW_FOCUSED | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
| AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
| AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
private static final int PASSWORD_MANAGER_FLAG_TYPE_MASK = AccessibilityServiceInfo.DEFAULT
| AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
| AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE
| AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY
| AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
| AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
private static final int PASSWORD_MANAGER_CAPABILITY_TYPE_MASK =
AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT;
// A bitmask containing the union of all event types, feedback types, flags,
// and capabilities of running accessibility services.
private static int sEventTypeMask;
private static int sFeedbackTypeMask;
private static int sFlagsMask;
private static int sCapabilitiesMask;
private static State sState;
private static boolean sInitialized;
// Observers for various System, Activity, and Settings states relevant to accessibility.
private static final ApplicationStatus.ActivityStateListener sActivityStateListener =
AccessibilityState::onActivityStateChange;
private static final ApplicationStatus.ApplicationStateListener sApplicationStateListener =
AccessibilityState::onApplicationStateChange;
private static ServicesObserver sAccessibilityServicesObserver;
private static ServicesObserver sAnimationDurationScaleObserver;
/**
* Whether the user has enabled the Android-OS speak password when in accessibility mode,
* available on pre-Android O. (Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD).
*
* From Android docs:
* @deprecated The speaking of passwords is controlled by individual accessibility services.
* Apps should ignore this setting and provide complete information to accessibility
* at all times, which was the behavior when this value was {@code true}.
*/
@Deprecated
private static boolean sAccessibilitySpeakPasswordEnabled;
// The recommended multiplier to apply to a timeout for changes to the UI needed by the user.
// Uses all content flags, cached from {AccessibilityManager#getRecommendedTimeoutMillis}.
// Requires Android Q, for versions < Q, this will always be equal to 1.0f.
private static float sRecommendedTimeoutMultiplier;
// The IDs of all running accessibility services.
private static String[] sServiceIds;
// The set of listeners of AccessibilityState, implemented using
// a WeakHashSet behind the scenes so that listeners can be garbage-collected
// and will be automatically removed from this set.
private static final Set<Listener> sListeners =
Collections.newSetFromMap(new WeakHashMap<Listener, Boolean>());
// The number of milliseconds to wait before checking the set of running accessibility services
// again, when we think it changed. Uses an exponential back-off until it's greater than
// MAX_DELAY_MILLIS. Note that each delay is additive, so the total time for a guaranteed signal
// to listener is ~7.5 seconds.
private static final int MIN_DELAY_MILLIS = 250;
private static final int MAX_DELAY_MILLIS = 5000;
private static int sNextDelayMillis = MIN_DELAY_MILLIS;
public static void addListener(Listener listener) {
sListeners.add(listener);
}
public static boolean isScreenReaderEnabled() {
if (!sInitialized) updateAccessibilityServices();
return sState.isScreenReaderEnabled;
}
public static boolean isTouchExplorationEnabled() {
if (!sInitialized) updateAccessibilityServices();
return sState.isTouchExplorationEnabled;
}
public static boolean isPerformGesturesEnabled() {
if (!sInitialized) updateAccessibilityServices();
return sState.isPerformGesturesEnabled;
}
public static boolean isAnyAccessibilityServiceEnabled() {
if (!sInitialized) updateAccessibilityServices();
return sState.isAnyAccessibilityServiceEnabled;
}
public static boolean isAccessibilityToolPresent() {
if (!sInitialized) updateAccessibilityServices();
return sState.isAccessibilityToolPresent;
}
public static boolean isSpokenFeedbackServicePresent() {
if (!sInitialized) updateAccessibilityServices();
return sState.isSpokenFeedbackServicePresent;
}
public static boolean isTextShowPasswordEnabled() {
if (!sInitialized) updateAccessibilityServices();
return sState.isTextShowPasswordEnabled;
}
public static boolean isOnlyPasswordManagersEnabled() {
if (!sInitialized) updateAccessibilityServices();
return sState.isOnlyPasswordManagersEnabled;
}
@Deprecated
public static boolean isAccessibilitySpeakPasswordEnabled() {
if (!sInitialized) updateAccessibilityServices();
return sAccessibilitySpeakPasswordEnabled;
}
public static float getRecommendedTimeoutMultiplier() {
if (!sInitialized) updateAccessibilityServices();
return sRecommendedTimeoutMultiplier;
}
static void updateAccessibilityServices() {
if (!sInitialized) {
sState = new State(false, false, false, false, false, false, false, false);
}
sInitialized = true;
sEventTypeMask = 0;
sFeedbackTypeMask = 0;
sFlagsMask = 0;
sCapabilitiesMask = 0;
// Used solely as part of the heuristic to identify whether screen readers are running.
// This mask is kept separate from the above masks as those should be the source of
// truth.
int screenReaderCheckEventTypeMask = 0;
// Used solely as part of the heuristic to identify whether password managers are running.
// These masks are kept separate from the above masks as those should be the source of
// truth.
int passwordCheckEventTypeMask = 0;
int passwordCheckFeedbackTypeMask = 0;
int passwordCheckFlagsMask = 0;
int passwordCheckCapabilitiesMask = 0;
boolean isAnyAccessibilityServiceEnabled = false;
boolean isAccessibilityToolPresent = false;
// Get the list of currently running accessibility services.
Context context = ContextUtils.getApplicationContext();
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
sRecommendedTimeoutMultiplier = 1.0f;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
sRecommendedTimeoutMultiplier =
accessibilityManager.getRecommendedTimeoutMillis(
100, FLAG_CONTENT_ICONS | FLAG_CONTENT_TEXT | FLAG_CONTENT_CONTROLS)
/ 100.0f;
}
List<AccessibilityServiceInfo> services =
accessibilityManager.getEnabledAccessibilityServiceList(
AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
sServiceIds = new String[services.size()];
ArrayList<String> runningServiceNames = new ArrayList<String>();
int i = 0;
for (AccessibilityServiceInfo service : services) {
if (service == null) continue;
isAccessibilityToolPresent |= (Build.VERSION.SDK_INT < Build.VERSION_CODES.S
|| service.isAccessibilityTool());
isAnyAccessibilityServiceEnabled = true;
String serviceId = service.getId();
sServiceIds[i++] = serviceId;
sEventTypeMask |= service.eventTypes;
sFeedbackTypeMask |= service.feedbackType;
sFlagsMask |= service.flags;
sCapabilitiesMask |= service.getCapabilities();
// Only check the event, feedback, flag, and capability types for the password manager
// heuristic if the running service is not the AutofillCompatAccessibilityService. The
// AutofillCompatAccessibilityService requests all events like a screenreader but
// does not serve assistive technology. It only serves autofill applications. The
// AutofillCompatAccessibilityService event mask would prevent the form controls
// heuristic from identifying the presence of other assistive technologies, so skip
// the mask for this service.
if (!serviceId.equals(AUTOFILL_COMPAT_ACCESSIBILITY_SERVICE_ID)) {
screenReaderCheckEventTypeMask |= service.eventTypes;
passwordCheckEventTypeMask |= service.eventTypes;
passwordCheckFeedbackTypeMask |= service.feedbackType;
passwordCheckFlagsMask |= service.flags;
passwordCheckCapabilitiesMask |= service.getCapabilities();
}
// Try to canonicalize the component name.
ComponentName componentName = ComponentName.unflattenFromString(serviceId);
if (componentName != null) {
runningServiceNames.add(componentName.flattenToShortString());
} else {
runningServiceNames.add(serviceId);
}
}
// Update the user password show/speak preferences.
int textShowPasswordSetting = Settings.System.getInt(
context.getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, 1);
boolean isTextShowPasswordEnabled = textShowPasswordSetting == 1;
int accessibilitySpeakPasswordSetting = Settings.Secure.getInt(
context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0);
sAccessibilitySpeakPasswordEnabled = accessibilitySpeakPasswordSetting == 1;
// Get the list of enabled accessibility services, from settings, in
// case it's different.
ArrayList<String> enabledServiceNames = new ArrayList<String>();
String serviceNamesString = Settings.Secure.getString(
context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
if (serviceNamesString != null && !serviceNamesString.isEmpty()) {
String[] serviceNames = serviceNamesString.split(":");
for (String name : serviceNames) {
// null or empty names can be skipped
if (name == null || name.isEmpty()) continue;
// Try to canonicalize the component name if possible.
ComponentName componentName = ComponentName.unflattenFromString(name);
if (componentName != null) {
enabledServiceNames.add(componentName.flattenToShortString());
} else {
enabledServiceNames.add(name);
}
}
}
// Compare the list of enabled package names to the list of running package names.
// When the system setting containing the list of running accessibility services
// changes, it isn't always reflected in getEnabledAccessibilityServiceList
// immediately. To ensure we always have an up-to-date value, check that the
// set of services match, and if they don't, schedule an update with an exponential
// back-off.
Collections.sort(runningServiceNames);
Collections.sort(enabledServiceNames);
if (runningServiceNames.equals(enabledServiceNames)) {
Log.v(TAG, "Enabled accessibility services list updated.");
sNextDelayMillis = MIN_DELAY_MILLIS;
} else {
Log.v(TAG, "Enabled accessibility services: " + enabledServiceNames.toString());
Log.v(TAG, "Running accessibility services: " + runningServiceNames.toString());
// Do not inform listeners until the services agree, unless the limit set by
// {MAX_DELAY_MILLIS} has been reached, in which case send whatever we have.
if (sNextDelayMillis < MAX_DELAY_MILLIS) {
Log.v(TAG, "Will check again after " + sNextDelayMillis + " milliseconds.");
ThreadUtils.getUiThreadHandler().postDelayed(
AccessibilityState::updateAccessibilityServices, sNextDelayMillis);
sNextDelayMillis *= 2;
return;
} else {
Log.v(TAG, "Max delay reached. Send information as is.");
// Reset if we have reached {MAX_DELAY_MILLIS} so we do not miss later discrepancies
// between the sservices.
sNextDelayMillis = MIN_DELAY_MILLIS;
}
}
// If there are some events, flags, and capabilities enabled
// and if there are, at most, the expected set of password manager event, flags, and
// capabilities enabled, then the system is probably running only password managers
boolean areOnlyPasswordManagerMasksRequestedByServices =
(passwordCheckEventTypeMask != 0 && passwordCheckFlagsMask != 0
&& passwordCheckCapabilitiesMask != 0)
&& ((passwordCheckEventTypeMask | PASSWORD_MANAGER_EVENT_TYPE_MASK)
== PASSWORD_MANAGER_EVENT_TYPE_MASK)
&& ((passwordCheckFlagsMask | PASSWORD_MANAGER_FLAG_TYPE_MASK)
== PASSWORD_MANAGER_FLAG_TYPE_MASK)
&& ((passwordCheckCapabilitiesMask | PASSWORD_MANAGER_CAPABILITY_TYPE_MASK)
== PASSWORD_MANAGER_CAPABILITY_TYPE_MASK)
&& ((passwordCheckFeedbackTypeMask | AccessibilityServiceInfo.FEEDBACK_GENERIC)
== AccessibilityServiceInfo.FEEDBACK_GENERIC);
boolean isOnlyAutofillRunning = false;
// Only explicitly check for Autofill on compatible versions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillManager autofillManager = context.getSystemService(AutofillManager.class);
if (autofillManager != null && autofillManager.isEnabled()
&& autofillManager.hasEnabledAutofillServices()) {
// Confirm that autofill service is the only service running that requires
// accessibility.
if (runningServiceNames.isEmpty()
|| (runningServiceNames.size() == 1
&& runningServiceNames.get(0).equals(
AUTOFILL_COMPAT_ACCESSIBILITY_SERVICE_ID))) {
isOnlyAutofillRunning = true;
}
}
}
boolean isOnlyPasswordManagersEnabled = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// If build is >= S, then check if there are no accessibility tools present, then turn
// on form controls mode if the heuristic indicates that only password managers are
// enabled or Autofill is the only service running.
isOnlyPasswordManagersEnabled = !isAccessibilityToolPresent
&& (areOnlyPasswordManagerMasksRequestedByServices || isOnlyAutofillRunning);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// If build is >= O and < S, isAccessibilityToolPresent will always be true.
// Turn on form controls mode if the heuristic indicates that only password managers are
// enabled or Autofill is the only service running.
isOnlyPasswordManagersEnabled =
areOnlyPasswordManagerMasksRequestedByServices || isOnlyAutofillRunning;
} else {
// If the build is < O, isAccessibilityToolPresent will always be true and
// isOnlyAutofillRunning will always be false. Turn on form controls mode if the
// heuristic indicates that only password managers are enabled.
isOnlyPasswordManagersEnabled = areOnlyPasswordManagerMasksRequestedByServices;
}
// Update all listeners that there was a state change and pass whether or not the
// new state includes a screen reader.
Log.v(TAG, "Informing listeners of changes.");
boolean isScreenReaderEnabled =
(0 != (screenReaderCheckEventTypeMask & SCREEN_READER_EVENT_TYPE_MASK));
boolean isSpokenFeedbackServicePresent = (0 != (sFeedbackTypeMask & FEEDBACK_SPOKEN));
boolean isTouchExplorationEnabled =
(0 != (sFlagsMask & FLAG_REQUEST_TOUCH_EXPLORATION_MODE));
boolean isPerformGesturesEnabled =
(0 != (sCapabilitiesMask & CAPABILITY_CAN_PERFORM_GESTURES));
updateAndNotifyStateChange(new State(isScreenReaderEnabled, isTouchExplorationEnabled,
isPerformGesturesEnabled, isAnyAccessibilityServiceEnabled,
isAccessibilityToolPresent, isSpokenFeedbackServicePresent,
isTextShowPasswordEnabled, isOnlyPasswordManagersEnabled));
}
private static void updateAndNotifyStateChange(State newState) {
State oldState = sState;
sState = newState;
for (Listener listener : sListeners) {
listener.onAccessibilityStateChanged(oldState, newState);
}
}
/**
* Return a bitmask containing the union of all event types that running accessibility
* services listen to.
* @return
*/
// TODO(mschillaci,jacklynch): Make this private and update current callers.
@CalledByNative
public static int getAccessibilityServiceEventTypeMask() {
if (!sInitialized) updateAccessibilityServices();
return sEventTypeMask;
}
/**
* Return a bitmask containing the union of all feedback types that running accessibility
* services provide.
* @return
*/
@CalledByNative
private static int getAccessibilityServiceFeedbackTypeMask() {
if (!sInitialized) updateAccessibilityServices();
return sFeedbackTypeMask;
}
/**
* Return a bitmask containing the union of all flags from running accessibility services.
* @return
*/
@CalledByNative
private static int getAccessibilityServiceFlagsMask() {
if (!sInitialized) updateAccessibilityServices();
return sFlagsMask;
}
/**
* Return a bitmask containing the union of all service capabilities from running
* accessibility services.
* @return
*/
@CalledByNative
private static int getAccessibilityServiceCapabilitiesMask() {
if (!sInitialized) updateAccessibilityServices();
return sCapabilitiesMask;
}
/**
* Return a list of ids of all running accessibility services.
* @return
*/
@CalledByNative
private static String[] getAccessibilityServiceIds() {
if (!sInitialized) updateAccessibilityServices();
return sServiceIds;
}
/**
* Register observers of various system properties and initialize a state for clients.
*
* Note: This should only be called once, and before any client queries of accessibility state.
* The first time any client queries the state, |this| will be initialized.
*/
public static void registerObservers() {
assert !sInitialized
: "AccessibilityState has been called to register observers, but observers have "
+ "already been registered, or, a client has already queried the state. Observers "
+ "should only be registered once during browser init and before any client queries.";
ContentResolver contentResolver = ContextUtils.getApplicationContext().getContentResolver();
sAnimationDurationScaleObserver = new ServicesObserver(ThreadUtils.getUiThreadHandler(),
() -> AccessibilityStateJni.get().onAnimatorDurationScaleChanged());
sAccessibilityServicesObserver = new ServicesObserver(
ThreadUtils.getUiThreadHandler(), AccessibilityState::processServicesChange);
// We want to be notified whenever the user has updated the animator duration scale.
contentResolver.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), false,
sAnimationDurationScaleObserver);
// We want to be notified whenever the currently enabled services changes.
contentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES), false,
sAccessibilityServicesObserver);
contentResolver.registerContentObserver(
Settings.System.getUriFor(Settings.Secure.TOUCH_EXPLORATION_ENABLED), false,
sAccessibilityServicesObserver);
// We want to be notified if the user changes their preferred password show/speak settings.
contentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD), false,
sAccessibilityServicesObserver);
contentResolver.registerContentObserver(
Settings.System.getUriFor(Settings.System.TEXT_SHOW_PASSWORD), false,
sAccessibilityServicesObserver);
}
public static void initializeOnStartup() {
// This method is called as a deferred task during browser init. If no services are enabled,
// this will ensure the state is populated for any client queries later. If a service is
// enabled during startup, the current state may be queried before this method is called,
// in which case another update is not needed.
if (!sInitialized) {
updateAccessibilityServices();
}
// We want to be notified whenever an Activity or Application state changes.
ApplicationStatus.registerStateListenerForAllActivities(sActivityStateListener);
ApplicationStatus.registerApplicationStateListener(sApplicationStateListener);
// Histograms are recorded once during startup, and any time services change afterwards.
AccessibilityStateJni.get().recordAccessibilityServiceInfoHistograms();
}
private static void onActivityStateChange(Activity activity, int newState) {
// If Chrome is sent to the background, we will unregister observers, and re-register the
// observers and query state when Chrome is brought back to the foreground.
if (newState == ActivityState.RESUMED) processServicesChange();
}
private static void onApplicationStateChange(int newState) {
// If Chrome is sent to the background, we will unregister observers, and re-register the
// observers when Chrome is brought back to the foreground.
if (newState != ApplicationState.HAS_RUNNING_ACTIVITIES
&& newState != ApplicationState.HAS_PAUSED_ACTIVITIES) {
unregisterObservers();
} else if (newState == ApplicationState.HAS_RUNNING_ACTIVITIES && !sInitialized) {
registerObservers();
}
}
private static void unregisterObservers() {
ContentResolver contentResolver = ContextUtils.getApplicationContext().getContentResolver();
contentResolver.unregisterContentObserver(sAccessibilityServicesObserver);
contentResolver.unregisterContentObserver(sAnimationDurationScaleObserver);
sState = null;
sInitialized = false;
}
private static void processServicesChange() {
updateAccessibilityServices();
AccessibilityStateJni.get().recordAccessibilityServiceInfoHistograms();
}
private static class ServicesObserver extends ContentObserver {
private final Runnable mRunnable;
public ServicesObserver(Handler handler, Runnable runnable) {
super(handler);
mRunnable = runnable;
}
@Override
public void onChange(boolean selfChange) {
onChange(selfChange, null);
}
@Override
public void onChange(boolean selfChange, @Nullable Uri uri) {
ThreadUtils.getUiThreadHandler().post(mRunnable);
}
}
@NativeMethods
interface Natives {
void onAnimatorDurationScaleChanged();
void recordAccessibilityServiceInfoHistograms();
}
// ForTesting methods.
// clang-format off
@VisibleForTesting
public static void setIsScreenReaderEnabledForTesting(boolean enabled) {
if (!sInitialized) updateAccessibilityServices();
State newState = new State(
enabled,
sState.isTouchExplorationEnabled,
sState.isPerformGesturesEnabled,
sState.isAnyAccessibilityServiceEnabled,
sState.isAccessibilityToolPresent,
sState.isSpokenFeedbackServicePresent,
sState.isTextShowPasswordEnabled,
sState.isOnlyPasswordManagersEnabled);
updateAndNotifyStateChange(newState);
}
@VisibleForTesting
public static void setIsTouchExplorationEnabledForTesting(boolean enabled) {
if (!sInitialized) updateAccessibilityServices();
State newState = new State(
sState.isScreenReaderEnabled,
enabled,
sState.isPerformGesturesEnabled,
sState.isAnyAccessibilityServiceEnabled,
sState.isAccessibilityToolPresent,
sState.isSpokenFeedbackServicePresent,
sState.isTextShowPasswordEnabled,
sState.isOnlyPasswordManagersEnabled);
updateAndNotifyStateChange(newState);
}
@VisibleForTesting
public static void setIsPerformGesturesEnabledForTesting(boolean enabled) {
if (!sInitialized) updateAccessibilityServices();
State newState = new State(
sState.isScreenReaderEnabled,
sState.isTouchExplorationEnabled,
enabled,
sState.isAnyAccessibilityServiceEnabled,
sState.isAccessibilityToolPresent,
sState.isSpokenFeedbackServicePresent,
sState.isTextShowPasswordEnabled,
sState.isOnlyPasswordManagersEnabled);
updateAndNotifyStateChange(newState);
}
@VisibleForTesting
public static void setIsAnyAccessibilityServiceEnabledForTesting(boolean enabled) {
if (!sInitialized) updateAccessibilityServices();
State newState = new State(
sState.isScreenReaderEnabled,
sState.isTouchExplorationEnabled,
sState.isPerformGesturesEnabled,
enabled,
sState.isAccessibilityToolPresent,
sState.isSpokenFeedbackServicePresent,
sState.isTextShowPasswordEnabled,
sState.isOnlyPasswordManagersEnabled);
updateAndNotifyStateChange(newState);
}
@VisibleForTesting
public static void setIsAccessibilityToolPresentForTesting(boolean enabled) {
if (!sInitialized) updateAccessibilityServices();
State newState = new State(
sState.isScreenReaderEnabled,
sState.isTouchExplorationEnabled,
sState.isPerformGesturesEnabled,
sState.isAnyAccessibilityServiceEnabled,
enabled,
sState.isSpokenFeedbackServicePresent,
sState.isTextShowPasswordEnabled,
sState.isOnlyPasswordManagersEnabled);
updateAndNotifyStateChange(newState);
}
@VisibleForTesting
public static void setIsSpokenFeedbackServicePresentForTesting(boolean enabled) {
if (!sInitialized) updateAccessibilityServices();
State newState = new State(
sState.isScreenReaderEnabled,
sState.isTouchExplorationEnabled,
sState.isPerformGesturesEnabled,
sState.isAnyAccessibilityServiceEnabled,
sState.isAccessibilityToolPresent,
enabled,
sState.isTextShowPasswordEnabled,
sState.isOnlyPasswordManagersEnabled);
updateAndNotifyStateChange(newState);
}
@VisibleForTesting
public static void setIsTextShowPasswordEnabledForTesting(boolean enabled) {
if (!sInitialized) updateAccessibilityServices();
State newState = new State(
sState.isScreenReaderEnabled,
sState.isTouchExplorationEnabled,
sState.isPerformGesturesEnabled,
sState.isAnyAccessibilityServiceEnabled,
sState.isAccessibilityToolPresent,
sState.isSpokenFeedbackServicePresent,
enabled,
sState.isOnlyPasswordManagersEnabled);
updateAndNotifyStateChange(newState);
}
@VisibleForTesting
public static void setIsOnlyPasswordManagersEnabledForTesting(boolean enabled) {
if (!sInitialized) updateAccessibilityServices();
State newState = new State(
sState.isScreenReaderEnabled,
sState.isTouchExplorationEnabled,
sState.isPerformGesturesEnabled,
sState.isAnyAccessibilityServiceEnabled,
sState.isAccessibilityToolPresent,
sState.isSpokenFeedbackServicePresent,
sState.isTextShowPasswordEnabled,
enabled);
updateAndNotifyStateChange(newState);
}
@VisibleForTesting
public static void setEventTypeMaskForTesting(int mask) {
if (!sInitialized) updateAccessibilityServices();
// Explicitly set mask so events can be (ir)relevant to currently enabled service.
sEventTypeMask = mask;
}
@VisibleForTesting
public static void setRecommendedTimeoutMultiplierForTesting(float multiplier) {
if (!sInitialized) updateAccessibilityServices();
sRecommendedTimeoutMultiplier = multiplier;
}
// clang-format on
}