Merge "Revert "Enable background restrictions""
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
index 07a8253..b76aeb7 100644
--- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -414,9 +414,9 @@
public int flags;
/**
- * The unique string Id to identify the accessibility service.
+ * The component name the accessibility service.
*/
- private String mId;
+ private ComponentName mComponentName;
/**
* The Service that implements this accessibility service component.
@@ -464,7 +464,7 @@
public AccessibilityServiceInfo(ResolveInfo resolveInfo, Context context)
throws XmlPullParserException, IOException {
ServiceInfo serviceInfo = resolveInfo.serviceInfo;
- mId = new ComponentName(serviceInfo.packageName, serviceInfo.name).flattenToShortString();
+ mComponentName = new ComponentName(serviceInfo.packageName, serviceInfo.name);
mResolveInfo = resolveInfo;
XmlResourceParser parser = null;
@@ -574,7 +574,14 @@
* @hide
*/
public void setComponentName(ComponentName component) {
- mId = component.flattenToShortString();
+ mComponentName = component;
+ }
+
+ /**
+ * @hide
+ */
+ public ComponentName getComponentName() {
+ return mComponentName;
}
/**
@@ -585,7 +592,7 @@
* @return The id.
*/
public String getId() {
- return mId;
+ return mComponentName.flattenToShortString();
}
/**
@@ -715,7 +722,7 @@
parcel.writeInt(feedbackType);
parcel.writeLong(notificationTimeout);
parcel.writeInt(flags);
- parcel.writeString(mId);
+ parcel.writeParcelable(mComponentName, flagz);
parcel.writeParcelable(mResolveInfo, 0);
parcel.writeString(mSettingsActivityName);
parcel.writeInt(mCapabilities);
@@ -729,7 +736,7 @@
feedbackType = parcel.readInt();
notificationTimeout = parcel.readLong();
flags = parcel.readInt();
- mId = parcel.readString();
+ mComponentName = parcel.readParcelable(this.getClass().getClassLoader());
mResolveInfo = parcel.readParcelable(null);
mSettingsActivityName = parcel.readString();
mCapabilities = parcel.readInt();
@@ -739,7 +746,7 @@
@Override
public int hashCode() {
- return 31 * 1 + ((mId == null) ? 0 : mId.hashCode());
+ return 31 * 1 + ((mComponentName == null) ? 0 : mComponentName.hashCode());
}
@Override
@@ -754,11 +761,11 @@
return false;
}
AccessibilityServiceInfo other = (AccessibilityServiceInfo) obj;
- if (mId == null) {
- if (other.mId != null) {
+ if (mComponentName == null) {
+ if (other.mComponentName != null) {
return false;
}
- } else if (!mId.equals(other.mId)) {
+ } else if (!mComponentName.equals(other.mComponentName)) {
return false;
}
return true;
@@ -777,7 +784,7 @@
stringBuilder.append(", ");
appendFlags(stringBuilder, flags);
stringBuilder.append(", ");
- stringBuilder.append("id: ").append(mId);
+ stringBuilder.append("id: ").append(getId());
stringBuilder.append(", ");
stringBuilder.append("resolveInfo: ").append(mResolveInfo);
stringBuilder.append(", ");
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 893e53c..371c0f3 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5313,6 +5313,21 @@
public static final String ACCESSIBILITY_ENABLED = "accessibility_enabled";
/**
+ * Setting specifying if the accessibility shortcut dialog has been shown to this user.
+ * @hide
+ */
+ public static final String ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN =
+ "accessibility_shortcut_dialog_shown";
+
+ /**
+ * Setting specifying the the accessibility service to be toggled via the accessibility
+ * shortcut. Must be its flattened {@link ComponentName}.
+ * @hide
+ */
+ public static final String ACCESSIBILITY_SHORTCUT_TARGET_SERVICE =
+ "accessibility_shortcut_target_service";
+
+ /**
* If touch exploration is enabled.
*/
public static final String TOUCH_EXPLORATION_ENABLED = "touch_exploration_enabled";
@@ -6782,6 +6797,8 @@
TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
TOUCH_EXPLORATION_ENABLED,
ACCESSIBILITY_ENABLED,
+ ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+ ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
ACCESSIBILITY_SPEAK_PASSWORD,
ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED,
ACCESSIBILITY_CAPTIONING_PRESET,
@@ -7071,7 +7088,9 @@
* Setting whether the global gesture for enabling accessibility is enabled.
* If this gesture is enabled the user will be able to perfrom it to enable
* the accessibility state without visiting the settings app.
+ *
* @hide
+ * No longer used. Should be removed once all dependencies have been updated.
*/
public static final String ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED =
"enable_accessibility_global_gesture_enabled";
@@ -9372,7 +9391,6 @@
DOCK_SOUNDS_ENABLED,
CHARGING_SOUNDS_ENABLED,
USB_MASS_STORAGE_ENABLED,
- ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED,
WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON,
WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY,
WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED,
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index 6d2f850..0e753f3 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -83,6 +83,13 @@
private static final int GLOBAL_ACTIONS_KEY_TIMEOUT = 500;
/**
+ * Defines the duration in milliseconds a user needs to hold down the
+ * appropriate button to bring up the accessibility shortcut (first time) or enable it
+ * (once shortcut is configured).
+ */
+ private static final int A11Y_SHORTCUT_KEY_TIMEOUT = 3000;
+
+ /**
* Defines the duration in milliseconds we will wait to see if a touch event
* is a tap or a scroll. If the user does not move within this interval, it is
* considered to be a tap.
@@ -785,6 +792,18 @@
}
/**
+ * The amount of time a user needs to press the relevant keys to activate the accessibility
+ * shortcut.
+ *
+ * @return how long a user needs to press the relevant keys to activate the accessibility
+ * shortcut.
+ * @hide
+ */
+ public long getAccessibilityShortcutKeyTimeout() {
+ return A11Y_SHORTCUT_KEY_TIMEOUT;
+ }
+
+ /**
* The amount of friction applied to scrolls and flings.
*
* @return A scalar dimensionless value representing the coefficient of
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 7f940f1..bfb8d83 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -21,6 +21,7 @@
import android.Manifest;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.NonNull;
+import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
@@ -241,6 +242,8 @@
* @hide
*/
public AccessibilityManager(Context context, IAccessibilityManager service, int userId) {
+ // Constructor can't be chained because we can't create an instance of an inner class
+ // before calling another constructor.
mHandler = new MyHandler(context.getMainLooper());
mUserId = userId;
synchronized (mLock) {
@@ -249,6 +252,23 @@
}
/**
+ * Create an instance.
+ *
+ * @param handler The handler to use
+ * @param service An interface to the backing service.
+ * @param userId User id under which to run.
+ *
+ * @hide
+ */
+ public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) {
+ mHandler = handler;
+ mUserId = userId;
+ synchronized (mLock) {
+ tryConnectToServiceLocked(service);
+ }
+ }
+
+ /**
* @hide
*/
public IAccessibilityManagerClient getClient() {
@@ -647,6 +667,30 @@
}
/**
+ * Find an installed service with the specified {@link ComponentName}.
+ *
+ * @param componentName The name to match to the service.
+ *
+ * @return The info corresponding to the installed service, or {@code null} if no such service
+ * is installed.
+ * @hide
+ */
+ public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName(
+ ComponentName componentName) {
+ final List<AccessibilityServiceInfo> installedServiceInfos =
+ getInstalledAccessibilityServiceList();
+ if ((installedServiceInfos == null) || (componentName == null)) {
+ return null;
+ }
+ for (int i = 0; i < installedServiceInfos.size(); i++) {
+ if (componentName.equals(installedServiceInfos.get(i).getComponentName())) {
+ return installedServiceInfos.get(i);
+ }
+ }
+ return null;
+ }
+
+ /**
* Adds an accessibility interaction connection interface for a given window.
* @param windowToken The window token to which a connection is added.
* @param connection The connection.
@@ -693,6 +737,26 @@
}
}
+ /**
+ * Perform the accessibility shortcut if the caller has permission.
+ *
+ * @hide
+ */
+ public void performAccessibilityShortcut() {
+ final IAccessibilityManager service;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return;
+ }
+ }
+ try {
+ service.performAccessibilityShortcut();
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re);
+ }
+ }
+
private IAccessibilityManager getServiceLocked() {
if (mService == null) {
tryConnectToServiceLocked(null);
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 2829744..ed77f68 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -60,7 +60,6 @@
IBinder getWindowToken(int windowId, int userId);
- void enableAccessibilityService(in ComponentName service, int userId);
-
- void disableAccessibilityService(in ComponentName service, int userId);
+ // Requires WRITE_SECURE_SETTINGS
+ void performAccessibilityShortcut();
}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index db157bf..7de48d3 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2710,4 +2710,10 @@
<!-- Component name of the default cell broadcast receiver -->
<string name="config_defaultCellBroadcastReceiverComponent" translatable="false">com.android.cellbroadcastreceiver/.PrivilegedCellBroadcastReceiver</string>
+
+ <!-- The component name, flattened to a string, for the default accessibility service to be
+ enabled by the accessibility shortcut. This service must be trusted, as it can be activated
+ without explicit consent of the user. If no accessibility service with the specified name
+ exists on the device, the accessibility shortcut will be disabled by default. -->
+ <string name="config_defaultAccessibilityService" translatable="false"></string>
</resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 87a4732..0204e93 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3818,12 +3818,35 @@
"Raise volume above recommended level?\n\nListening at high volume for long periods may damage your hearing."
</string>
- <!-- Text spoken when the user is performing a gesture that will enable accessibility. [CHAR LIMIT=none] -->
- <string name="continue_to_enable_accessibility">Keep holding down two fingers to enable accessibility.</string>
- <!-- Text spoken when the user enabled accessibility. [CHAR LIMIT=none] -->
- <string name="accessibility_enabled">Accessibility enabled.</string>
- <!-- Text spoken when the user stops preforming a gesture that would enable accessibility. [CHAR LIMIT=none] -->
- <string name="enable_accessibility_canceled">Accessibility canceled.</string>
+ <!-- Dialog title for dialog shown when the accessibility shortcut is activated, and we want
+ to confirm that the user understands what's going to happen-->
+ <string name="accessibility_shortcut_warning_dialog_title">Accessibility Shortcut is ON</string>
+
+ <!-- Message shown in dialog when user is in the process of enabling the accessibility
+ service via the volume buttons shortcut for the first time. [CHAR LIMIT=none] -->
+ <string name="accessibility_shortcut_toogle_warning">
+ Turn <xliff:g id="service_name" example="TalkBack">%1$s</xliff:g> on or off by holding down
+ both volume buttons for 3 seconds.\n\nYou can change the service in
+ Settings > Accessibility.
+ </string>
+
+ <!-- Text in button that turns off the accessibility shortcut -->
+ <string name="disable_accessibility_shortcut">Turn Off Shortcut</string>
+
+ <!-- Text in button that closes the warning dialog about the accessibility shortcut, leaving the
+ shortcut enabled.-->
+ <string name="leave_accessibility_shortcut_on">Leave on</string>
+
+ <!-- Text in toast to alert the user that the accessibility shortcut turned on an accessibility
+ service.-->
+ <string name="accessibility_shortcut_enabling_service">Accessibility Shortcut turned
+ <xliff:g id="service_name" example="TalkBack">%1$s</xliff:g> on</string>
+
+ <!-- Text in toast to alert the user that the accessibility shortcut turned off an accessibility
+ service.-->
+ <string name="accessibility_shortcut_disabling_service">Accessibility Shortcut turned
+ <xliff:g id="service_name" example="TalkBack">%1$s</xliff:g> off</string>
+
<!-- Text spoken when the current user is switched if accessibility is enabled. [CHAR LIMIT=none] -->
<string name="user_switched">Current user <xliff:g id="name" example="Bob">%1$s</xliff:g>.</string>
<!-- Message shown when switching to a user [CHAR LIMIT=none] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 0a75744..c370ef7 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1139,7 +1139,6 @@
<java-symbol type="string" name="conference_call" />
<java-symbol type="string" name="tooltip_popup_title" />
-
<java-symbol type="plurals" name="bugreport_countdown" />
<java-symbol type="plurals" name="last_num_days" />
<java-symbol type="plurals" name="matches_found" />
@@ -2798,4 +2797,13 @@
<java-symbol type="raw" name="fallback_categories" />
<java-symbol type="attr" name="primaryContentAlpha" />
+
+ <!-- Accessibility Shortcut -->
+ <java-symbol type="string" name="accessibility_shortcut_warning_dialog_title" />
+ <java-symbol type="string" name="accessibility_shortcut_toogle_warning" />
+ <java-symbol type="string" name="accessibility_shortcut_enabling_service" />
+ <java-symbol type="string" name="accessibility_shortcut_disabling_service" />
+ <java-symbol type="string" name="disable_accessibility_shortcut" />
+ <java-symbol type="string" name="leave_accessibility_shortcut_on" />
+ <java-symbol type="string" name="config_defaultAccessibilityService" />
</resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java b/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java
index fcff305..9bb3c36 100644
--- a/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java
@@ -28,6 +28,8 @@
import android.util.ArraySet;
import android.view.accessibility.AccessibilityManager;
+import com.android.internal.R;
+
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -147,6 +149,26 @@
enabledServicesBuilder.toString(), userId);
}
+ /**
+ * Get the name of the service that should be toggled by the accessibility shortcut. Use
+ * an OEM-configurable default if the setting has never been set.
+ *
+ * @param context A valid context
+ * @param userId The user whose settings should be checked
+ *
+ * @return The component name, flattened to a string, of the target service.
+ */
+ public static String getShortcutTargetServiceComponentNameString(
+ Context context, int userId) {
+ final String currentShortcutServiceId = Settings.Secure.getStringForUser(
+ context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+ userId);
+ if (currentShortcutServiceId != null) {
+ return currentShortcutServiceId;
+ }
+ return context.getString(R.string.config_defaultAccessibilityService);
+ }
+
private static Set<ComponentName> getInstalledServices(Context context) {
final Set<ComponentName> installedServices = new HashSet<>();
installedServices.clear();
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index b34e4e4..ece5149 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -97,6 +97,7 @@
import com.android.internal.os.SomeArgs;
import com.android.server.LocalServices;
+import com.android.server.policy.AccessibilityShortcutController;
import com.android.server.statusbar.StatusBarManagerInternal;
import org.xmlpull.v1.XmlPullParserException;
@@ -1489,6 +1490,7 @@
mInitialized = true;
updateLegacyCapabilitiesLocked(userState);
updateServicesLocked(userState);
+ updateAccessibilityShortcutLocked(userState);
updateWindowsForAccessibilityCallbackLocked(userState);
updateAccessibilityFocusBehaviorLocked(userState);
updateFilterKeyEventsLocked(userState);
@@ -1613,7 +1615,7 @@
somethingChanged |= readEnhancedWebAccessibilityEnabledChangedLocked(userState);
somethingChanged |= readDisplayMagnificationEnabledSettingLocked(userState);
somethingChanged |= readAutoclickEnabledSettingLocked(userState);
-
+ somethingChanged |= readAccessibilityShortcutSettingLocked(userState);
return somethingChanged;
}
@@ -1722,6 +1724,50 @@
}
}
+ private boolean readAccessibilityShortcutSettingLocked(UserState userState) {
+ String componentNameToEnableString = AccessibilityShortcutController
+ .getTargetServiceComponentNameString(mContext, userState.mUserId);
+ if ((componentNameToEnableString == null) || componentNameToEnableString.isEmpty()) {
+ if (userState.mServiceToEnableWithShortcut == null) {
+ return false;
+ }
+ userState.mServiceToEnableWithShortcut = null;
+ return true;
+ }
+ ComponentName componentNameToEnable =
+ ComponentName.unflattenFromString(componentNameToEnableString);
+ if (componentNameToEnable.equals(userState.mServiceToEnableWithShortcut)) {
+ return false;
+ }
+ userState.mServiceToEnableWithShortcut = componentNameToEnable;
+ return true;
+ }
+
+ /**
+ * Check if the service that will be enabled by the shortcut is installed. If it isn't,
+ * clear the value and the associated setting so a sideloaded service can't spoof the
+ * package name of the default service.
+ *
+ * @param userState
+ */
+ private void updateAccessibilityShortcutLocked(UserState userState) {
+ if (userState.mServiceToEnableWithShortcut == null) {
+ return;
+ }
+ boolean shortcutServiceIsInstalled = false;
+ for (int i = 0; i < userState.mInstalledServices.size(); i++) {
+ if (userState.mInstalledServices.get(i).getComponentName()
+ .equals(userState.mServiceToEnableWithShortcut)) {
+ shortcutServiceIsInstalled = true;
+ }
+ }
+ if (!shortcutServiceIsInstalled) {
+ userState.mServiceToEnableWithShortcut = null;
+ Settings.Secure.putStringForUser(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "", userState.mUserId);
+ }
+ }
+
private boolean canRequestAndRequestsTouchExplorationLocked(Service service) {
// Service not ready or cannot request the feature - well nothing to do.
if (!service.canReceiveEventsLocked() || !service.mRequestTouchExplorationMode) {
@@ -1895,44 +1941,63 @@
}
/**
+ * AIDL-exposed method to be called when the accessibility shortcut is enabled. Requires
+ * permission to write secure settings, since someone with that permission can enable
+ * accessibility services themselves.
+ */
+ public void performAccessibilityShortcut() {
+ if ((UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)
+ && (mContext.checkCallingPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ != PackageManager.PERMISSION_GRANTED)) {
+ throw new SecurityException(
+ "performAccessibilityShortcut requires the WRITE_SECURE_SETTINGS permission");
+ }
+ synchronized(mLock) {
+ UserState userState = getUserStateLocked(mCurrentUserId);
+ ComponentName serviceName = userState.mServiceToEnableWithShortcut;
+ if (serviceName == null) {
+ return;
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ if (userState.mComponentNameToServiceMap.get(serviceName) == null) {
+ enableAccessibilityServiceLocked(serviceName, mCurrentUserId);
+ } else {
+ disableAccessibilityServiceLocked(serviceName, mCurrentUserId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ };
+
+ /**
* Enables accessibility service specified by {@param componentName} for the {@param userId}.
*/
- public void enableAccessibilityService(ComponentName componentName, int userId) {
- synchronized(mLock) {
- if (Binder.getCallingUid() != Process.SYSTEM_UID) {
- throw new SecurityException("only SYSTEM can call enableAccessibilityService.");
- }
+ private void enableAccessibilityServiceLocked(ComponentName componentName, int userId) {
+ SettingsStringHelper settingsHelper = new SettingsStringHelper(
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, userId);
+ settingsHelper.addService(componentName);
+ settingsHelper.writeToSettings();
- SettingsStringHelper settingsHelper = new SettingsStringHelper(
- Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, userId);
- settingsHelper.addService(componentName);
- settingsHelper.writeToSettings();
-
- UserState userState = getUserStateLocked(userId);
- if (userState.mEnabledServices.add(componentName)) {
- onUserStateChangedLocked(userState);
- }
+ UserState userState = getUserStateLocked(userId);
+ if (userState.mEnabledServices.add(componentName)) {
+ onUserStateChangedLocked(userState);
}
}
/**
* Disables accessibility service specified by {@param componentName} for the {@param userId}.
*/
- public void disableAccessibilityService(ComponentName componentName, int userId) {
- synchronized(mLock) {
- if (Binder.getCallingUid() != Process.SYSTEM_UID) {
- throw new SecurityException("only SYSTEM can call disableAccessibility");
- }
+ private void disableAccessibilityServiceLocked(ComponentName componentName, int userId) {
+ SettingsStringHelper settingsHelper = new SettingsStringHelper(
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, userId);
+ settingsHelper.deleteService(componentName);
+ settingsHelper.writeToSettings();
- SettingsStringHelper settingsHelper = new SettingsStringHelper(
- Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, userId);
- settingsHelper.deleteService(componentName);
- settingsHelper.writeToSettings();
-
- UserState userState = getUserStateLocked(userId);
- if (userState.mEnabledServices.remove(componentName)) {
- onUserStateChangedLocked(userState);
- }
+ UserState userState = getUserStateLocked(userId);
+ if (userState.mEnabledServices.remove(componentName)) {
+ onUserStateChangedLocked(userState);
}
}
@@ -4307,6 +4372,8 @@
public ComponentName mServiceChangingSoftKeyboardMode;
+ public ComponentName mServiceToEnableWithShortcut;
+
public int mLastSentClientState = -1;
public int mSoftKeyboardShowMode = 0;
@@ -4439,6 +4506,9 @@
private final Uri mAccessibilitySoftKeyboardModeUri = Settings.Secure.getUriFor(
Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE);
+ private final Uri mAccessibilityShortcutServiceIdUri = Settings.Secure.getUriFor(
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
+
public AccessibilityContentObserver(Handler handler) {
super(handler);
}
@@ -4467,6 +4537,8 @@
mHighTextContrastUri, false, this, UserHandle.USER_ALL);
contentResolver.registerContentObserver(
mAccessibilitySoftKeyboardModeUri, false, this, UserHandle.USER_ALL);
+ contentResolver.registerContentObserver(
+ mAccessibilityShortcutServiceIdUri, false, this, UserHandle.USER_ALL);
}
@Override
@@ -4519,6 +4591,10 @@
notifySoftKeyboardShowModeChangedLocked(userState.mSoftKeyboardShowMode);
onUserStateChangedLocked(userState);
}
+ } else if (mAccessibilityShortcutServiceIdUri.equals(uri)) {
+ if (readAccessibilityShortcutSettingLocked(userState)) {
+ onUserStateChangedLocked(userState);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/policy/AccessibilityShortcutController.java b/services/core/java/com/android/server/policy/AccessibilityShortcutController.java
new file mode 100644
index 0000000..133881a
--- /dev/null
+++ b/services/core/java/com/android/server/policy/AccessibilityShortcutController.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.server.policy;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.ActivityManager;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.ContentObserver;
+import android.media.AudioAttributes;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+
+import android.widget.Toast;
+import com.android.internal.R;
+
+import java.util.List;
+
+import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
+
+/**
+ * Class to help manage the accessibility shortcut
+ */
+public class AccessibilityShortcutController {
+ private static final String TAG = "AccessibilityShortcutController";
+
+ private final Context mContext;
+ private AlertDialog mAlertDialog;
+ private boolean mIsShortcutEnabled;
+ // Visible for testing
+ public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
+
+ public static String getTargetServiceComponentNameString(
+ Context context, int userId) {
+ final String currentShortcutServiceId = Settings.Secure.getStringForUser(
+ context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+ userId);
+ if (currentShortcutServiceId != null) {
+ return currentShortcutServiceId;
+ }
+ return context.getString(R.string.config_defaultAccessibilityService);
+ }
+
+ public AccessibilityShortcutController(Context context, Handler handler) {
+ mContext = context;
+
+ // Keep track of state of shortcut
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE),
+ false,
+ new ContentObserver(handler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ onSettingsChanged();
+ }
+ },
+ UserHandle.USER_ALL);
+ updateShortcutEnabled();
+ }
+
+ public boolean isAccessibilityShortcutAvailable() {
+ return mIsShortcutEnabled;
+ }
+
+ public void onSettingsChanged() {
+ updateShortcutEnabled();
+ }
+
+ /**
+ * Called when the accessibility shortcut is activated
+ */
+ public void performAccessibilityShortcut() {
+ Slog.d(TAG, "Accessibility shortcut activated");
+ final ContentResolver cr = mContext.getContentResolver();
+ final int userId = ActivityManager.getCurrentUser();
+ final int dialogAlreadyShown = Settings.Secure.getIntForUser(
+ cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
+ final Ringtone tone =
+ RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI);
+ if (tone != null) {
+ tone.setAudioAttributes(new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
+ .build());
+ tone.play();
+ }
+ if (dialogAlreadyShown == 0) {
+ // The first time, we show a warning rather than toggle the service to give the user a
+ // chance to turn off this feature before stuff gets enabled.
+ mAlertDialog = createShortcutWarningDialog(userId);
+ if (mAlertDialog == null) {
+ return;
+ }
+ Window w = mAlertDialog.getWindow();
+ WindowManager.LayoutParams attr = w.getAttributes();
+ attr.type = TYPE_KEYGUARD_DIALOG;
+ w.setAttributes(attr);
+ mAlertDialog.show();
+ Settings.Secure.putIntForUser(
+ cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1, userId);
+ } else {
+ if (mAlertDialog != null) {
+ mAlertDialog.dismiss();
+ mAlertDialog = null;
+ }
+
+ // Show a toast alerting the user to what's happening
+ final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
+ if (serviceInfo == null) {
+ Slog.e(TAG, "Accessibility shortcut set to invalid service");
+ return;
+ }
+ String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
+ ? R.string.accessibility_shortcut_disabling_service
+ : R.string.accessibility_shortcut_enabling_service);
+ String toastMessage = String.format(toastMessageFormatString,
+ serviceInfo.getResolveInfo()
+ .loadLabel(mContext.getPackageManager()).toString());
+ mFrameworkObjectProvider.makeToastFromText(mContext, toastMessage, Toast.LENGTH_LONG)
+ .show();
+
+ mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
+ .performAccessibilityShortcut();
+ }
+ }
+
+ private void updateShortcutEnabled() {
+ mIsShortcutEnabled = !TextUtils.isEmpty(getTargetServiceComponentNameString(
+ mContext, UserHandle.myUserId()));
+ }
+
+ private AlertDialog createShortcutWarningDialog(int userId) {
+ final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
+
+ if (serviceInfo == null) {
+ return null;
+ }
+
+ final String warningMessage = String.format(
+ mContext.getString(R.string.accessibility_shortcut_toogle_warning),
+ serviceInfo.getResolveInfo().loadLabel(mContext.getPackageManager()).toString());
+ final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(mContext)
+ .setTitle(R.string.accessibility_shortcut_warning_dialog_title)
+ .setMessage(warningMessage)
+ .setCancelable(false)
+ .setPositiveButton(R.string.leave_accessibility_shortcut_on, null)
+ .setNegativeButton(R.string.disable_accessibility_shortcut,
+ (DialogInterface d, int which) -> {
+ Settings.Secure.putStringForUser(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
+ userId);
+ })
+ .setOnCancelListener((DialogInterface d) -> {
+ // If canceled, treat as if the dialog has never been shown
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
+ })
+ .create();
+ return alertDialog;
+ }
+
+ private AccessibilityServiceInfo getInfoForTargetService() {
+ final String currentShortcutServiceString = getTargetServiceComponentNameString(
+ mContext, UserHandle.myUserId());
+ if (currentShortcutServiceString == null) {
+ return null;
+ }
+ AccessibilityManager accessibilityManager =
+ mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
+ return accessibilityManager.getInstalledServiceInfoWithComponentName(
+ ComponentName.unflattenFromString(currentShortcutServiceString));
+ }
+
+ private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
+ AccessibilityManager accessibilityManager =
+ mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
+ return accessibilityManager.getEnabledAccessibilityServiceList(
+ AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo);
+ }
+
+ // Class to allow mocking of static framework calls
+ public static class FrameworkObjectProvider {
+ public AccessibilityManager getAccessibilityManagerInstance(Context context) {
+ return AccessibilityManager.getInstance(context);
+ }
+
+ public AlertDialog.Builder getAlertDialogBuilder(Context context) {
+ return new AlertDialog.Builder(context);
+ }
+
+ public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
+ return Toast.makeText(context, charSequence, duration);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/policy/EnableAccessibilityController.java b/services/core/java/com/android/server/policy/EnableAccessibilityController.java
deleted file mode 100644
index 6b203a9..0000000
--- a/services/core/java/com/android/server/policy/EnableAccessibilityController.java
+++ /dev/null
@@ -1,291 +0,0 @@
-/*
- * Copyright (C) 2012 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.server.policy;
-
-import android.accessibilityservice.AccessibilityService;
-import android.accessibilityservice.AccessibilityServiceInfo;
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.pm.ServiceInfo;
-import android.media.AudioManager;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.os.Handler;
-import android.os.Message;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.UserManager;
-import android.provider.Settings;
-import android.speech.tts.TextToSpeech;
-import android.util.Log;
-import android.util.MathUtils;
-import android.view.IWindowManager;
-import android.view.MotionEvent;
-import android.view.WindowManager;
-import android.view.WindowManagerGlobal;
-import android.view.WindowManagerInternal;
-import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.IAccessibilityManager;
-
-import com.android.internal.R;
-import com.android.server.LocalServices;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-public class EnableAccessibilityController {
- private static final String TAG = "EnableAccessibilityController";
-
- private static final int SPEAK_WARNING_DELAY_MILLIS = 2000;
- private static final int ENABLE_ACCESSIBILITY_DELAY_MILLIS = 6000;
-
- public static final int MESSAGE_SPEAK_WARNING = 1;
- public static final int MESSAGE_SPEAK_ENABLE_CANCELED = 2;
- public static final int MESSAGE_ENABLE_ACCESSIBILITY = 3;
-
- private final Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message message) {
- switch (message.what) {
- case MESSAGE_SPEAK_WARNING: {
- String text = mContext.getString(R.string.continue_to_enable_accessibility);
- mTts.speak(text, TextToSpeech.QUEUE_FLUSH, null);
- } break;
- case MESSAGE_SPEAK_ENABLE_CANCELED: {
- String text = mContext.getString(R.string.enable_accessibility_canceled);
- mTts.speak(text, TextToSpeech.QUEUE_FLUSH, null);
- } break;
- case MESSAGE_ENABLE_ACCESSIBILITY: {
- enableAccessibility();
- mTone.play();
- mTts.speak(mContext.getString(R.string.accessibility_enabled),
- TextToSpeech.QUEUE_FLUSH, null);
- } break;
- }
- }
- };
-
- private final IAccessibilityManager mAccessibilityManager = IAccessibilityManager
- .Stub.asInterface(ServiceManager.getService("accessibility"));
-
-
- private final Context mContext;
- private final Runnable mOnAccessibilityEnabledCallback;
- private final UserManager mUserManager;
- private final TextToSpeech mTts;
- private final Ringtone mTone;
-
- private final float mTouchSlop;
-
- private boolean mDestroyed;
- private boolean mCanceled;
-
- private float mFirstPointerDownX;
- private float mFirstPointerDownY;
- private float mSecondPointerDownX;
- private float mSecondPointerDownY;
-
- public EnableAccessibilityController(Context context, Runnable onAccessibilityEnabledCallback) {
- mContext = context;
- mOnAccessibilityEnabledCallback = onAccessibilityEnabledCallback;
- mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
- mTts = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
- @Override
- public void onInit(int status) {
- if (mDestroyed) {
- mTts.shutdown();
- }
- }
- });
- mTone = RingtoneManager.getRingtone(context, Settings.System.DEFAULT_NOTIFICATION_URI);
- mTone.setStreamType(AudioManager.STREAM_MUSIC);
- mTouchSlop = context.getResources().getDimensionPixelSize(
- R.dimen.accessibility_touch_slop);
- }
-
- public static boolean canEnableAccessibilityViaGesture(Context context) {
- AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(context);
- // Accessibility is enabled and there is an enabled speaking
- // accessibility service, then we have nothing to do.
- if (accessibilityManager.isEnabled()
- && !accessibilityManager.getEnabledAccessibilityServiceList(
- AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty()) {
- return false;
- }
- // If the global gesture is enabled and there is a speaking service
- // installed we are good to go, otherwise there is nothing to do.
- return Settings.Global.getInt(context.getContentResolver(),
- Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1
- && !getInstalledSpeakingAccessibilityServices(context).isEmpty();
- }
-
- public static List<AccessibilityServiceInfo> getInstalledSpeakingAccessibilityServices(
- Context context) {
- List<AccessibilityServiceInfo> services = new ArrayList<AccessibilityServiceInfo>();
- services.addAll(AccessibilityManager.getInstance(context)
- .getInstalledAccessibilityServiceList());
- Iterator<AccessibilityServiceInfo> iterator = services.iterator();
- while (iterator.hasNext()) {
- AccessibilityServiceInfo service = iterator.next();
- if ((service.feedbackType & AccessibilityServiceInfo.FEEDBACK_SPOKEN) == 0) {
- iterator.remove();
- }
- }
- return services;
- }
-
- public void onDestroy() {
- mDestroyed = true;
- }
-
- public boolean onInterceptTouchEvent(MotionEvent event) {
- if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN
- && event.getPointerCount() == 2) {
- mFirstPointerDownX = event.getX(0);
- mFirstPointerDownY = event.getY(0);
- mSecondPointerDownX = event.getX(1);
- mSecondPointerDownY = event.getY(1);
- mHandler.sendEmptyMessageDelayed(MESSAGE_SPEAK_WARNING,
- SPEAK_WARNING_DELAY_MILLIS);
- mHandler.sendEmptyMessageDelayed(MESSAGE_ENABLE_ACCESSIBILITY,
- ENABLE_ACCESSIBILITY_DELAY_MILLIS);
- return true;
- }
- return false;
- }
-
- public boolean onTouchEvent(MotionEvent event) {
- final int pointerCount = event.getPointerCount();
- final int action = event.getActionMasked();
- if (mCanceled) {
- if (action == MotionEvent.ACTION_UP) {
- mCanceled = false;
- }
- return true;
- }
- switch (action) {
- case MotionEvent.ACTION_POINTER_DOWN: {
- if (pointerCount > 2) {
- cancel();
- }
- } break;
- case MotionEvent.ACTION_MOVE: {
- final float firstPointerMove = MathUtils.dist(event.getX(0),
- event.getY(0), mFirstPointerDownX, mFirstPointerDownY);
- if (Math.abs(firstPointerMove) > mTouchSlop) {
- cancel();
- }
- final float secondPointerMove = MathUtils.dist(event.getX(1),
- event.getY(1), mSecondPointerDownX, mSecondPointerDownY);
- if (Math.abs(secondPointerMove) > mTouchSlop) {
- cancel();
- }
- } break;
- case MotionEvent.ACTION_POINTER_UP:
- case MotionEvent.ACTION_CANCEL: {
- cancel();
- } break;
- }
- return true;
- }
-
- private void cancel() {
- mCanceled = true;
- if (mHandler.hasMessages(MESSAGE_SPEAK_WARNING)) {
- mHandler.removeMessages(MESSAGE_SPEAK_WARNING);
- } else if (mHandler.hasMessages(MESSAGE_ENABLE_ACCESSIBILITY)) {
- mHandler.sendEmptyMessage(MESSAGE_SPEAK_ENABLE_CANCELED);
- }
- mHandler.removeMessages(MESSAGE_ENABLE_ACCESSIBILITY);
- }
-
- private void enableAccessibility() {
- if (enableAccessibility(mContext)) {
- mOnAccessibilityEnabledCallback.run();
- }
- }
-
- public static boolean enableAccessibility(Context context) {
- final IAccessibilityManager accessibilityManager = IAccessibilityManager
- .Stub.asInterface(ServiceManager.getService("accessibility"));
- final WindowManagerInternal windowManager = LocalServices.getService(
- WindowManagerInternal.class);
- final UserManager userManager = (UserManager) context.getSystemService(
- Context.USER_SERVICE);
- ComponentName componentName = getInstalledSpeakingAccessibilityServiceComponent(context);
- if (componentName == null) {
- return false;
- }
-
- boolean keyguardLocked = windowManager.isKeyguardLocked();
- final boolean hasMoreThanOneUser = userManager.getUsers().size() > 1;
- try {
- if (!keyguardLocked || !hasMoreThanOneUser) {
- final int userId = ActivityManager.getCurrentUser();
- accessibilityManager.enableAccessibilityService(componentName, userId);
- } else if (keyguardLocked) {
- accessibilityManager.temporaryEnableAccessibilityStateUntilKeyguardRemoved(
- componentName, true /* enableTouchExploration */);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "cannot enable accessibilty: " + e);
- }
-
- return true;
- }
-
- public static void disableAccessibility(Context context) {
- final IAccessibilityManager accessibilityManager = IAccessibilityManager
- .Stub.asInterface(ServiceManager.getService("accessibility"));
- ComponentName componentName = getInstalledSpeakingAccessibilityServiceComponent(context);
- if (componentName == null) {
- return;
- }
-
- final int userId = ActivityManager.getCurrentUser();
- try {
- accessibilityManager.disableAccessibilityService(componentName, userId);
- } catch (RemoteException e) {
- Log.e(TAG, "cannot disable accessibility " + e);
- }
- }
-
- public static boolean isAccessibilityEnabled(Context context) {
- final AccessibilityManager accessibilityManager =
- context.getSystemService(AccessibilityManager.class);
- List enabledServices = accessibilityManager.getEnabledAccessibilityServiceList(
- AccessibilityServiceInfo.FEEDBACK_SPOKEN);
- return enabledServices != null && !enabledServices.isEmpty();
- }
-
- @Nullable
- public static ComponentName getInstalledSpeakingAccessibilityServiceComponent(
- Context context) {
- List<AccessibilityServiceInfo> services =
- getInstalledSpeakingAccessibilityServices(context);
- if (services.isEmpty()) {
- return null;
- }
-
- ServiceInfo serviceInfo = services.get(0).getResolveInfo().serviceInfo;
- return new ComponentName(serviceInfo.packageName, serviceInfo.name);
- }
-}
diff --git a/services/core/java/com/android/server/policy/GlobalActions.java b/services/core/java/com/android/server/policy/GlobalActions.java
index d4adcc4..335a230 100644
--- a/services/core/java/com/android/server/policy/GlobalActions.java
+++ b/services/core/java/com/android/server/policy/GlobalActions.java
@@ -44,7 +44,6 @@
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
@@ -59,12 +58,9 @@
import android.util.ArraySet;
import android.util.Log;
import android.util.TypedValue;
-import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
-import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
@@ -1194,21 +1190,14 @@
private static final class GlobalActionsDialog extends Dialog implements DialogInterface {
private final Context mContext;
- private final int mWindowTouchSlop;
private final AlertController mAlert;
private final MyAdapter mAdapter;
- private EnableAccessibilityController mEnableAccessibilityController;
-
- private boolean mIntercepted;
- private boolean mCancelOnUp;
-
public GlobalActionsDialog(Context context, AlertParams params) {
super(context, getDialogTheme(context));
mContext = getContext();
mAlert = AlertController.create(mContext, this, getWindow());
mAdapter = (MyAdapter) params.mAdapter;
- mWindowTouchSlop = ViewConfiguration.get(context).getScaledWindowTouchSlop();
params.apply(mAlert);
}
@@ -1221,76 +1210,10 @@
@Override
protected void onStart() {
- // If global accessibility gesture can be performed, we will take care
- // of dismissing the dialog on touch outside. This is because the dialog
- // is dismissed on the first down while the global gesture is a long press
- // with two fingers anywhere on the screen.
- if (EnableAccessibilityController.canEnableAccessibilityViaGesture(mContext)) {
- mEnableAccessibilityController = new EnableAccessibilityController(mContext,
- new Runnable() {
- @Override
- public void run() {
- dismiss();
- }
- });
- super.setCanceledOnTouchOutside(false);
- } else {
- mEnableAccessibilityController = null;
- super.setCanceledOnTouchOutside(true);
- }
-
+ super.setCanceledOnTouchOutside(true);
super.onStart();
}
- @Override
- protected void onStop() {
- if (mEnableAccessibilityController != null) {
- mEnableAccessibilityController.onDestroy();
- }
- super.onStop();
- }
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (mEnableAccessibilityController != null) {
- final int action = event.getActionMasked();
- if (action == MotionEvent.ACTION_DOWN) {
- View decor = getWindow().getDecorView();
- final int eventX = (int) event.getX();
- final int eventY = (int) event.getY();
- if (eventX < -mWindowTouchSlop
- || eventY < -mWindowTouchSlop
- || eventX >= decor.getWidth() + mWindowTouchSlop
- || eventY >= decor.getHeight() + mWindowTouchSlop) {
- mCancelOnUp = true;
- }
- }
- try {
- if (!mIntercepted) {
- mIntercepted = mEnableAccessibilityController.onInterceptTouchEvent(event);
- if (mIntercepted) {
- final long now = SystemClock.uptimeMillis();
- event = MotionEvent.obtain(now, now,
- MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
- event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- mCancelOnUp = true;
- }
- } else {
- return mEnableAccessibilityController.onTouchEvent(event);
- }
- } finally {
- if (action == MotionEvent.ACTION_UP) {
- if (mCancelOnUp) {
- cancel();
- }
- mCancelOnUp = false;
- mIntercepted = false;
- }
- }
- }
- return super.dispatchTouchEvent(event);
- }
-
public ListView getListView() {
return mAlert.getListView();
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 4b2b184..32b8c9b 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -149,8 +149,6 @@
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.IAudioService;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
import android.media.session.MediaSessionLegacyHelper;
import android.os.Binder;
import android.os.Build;
@@ -441,6 +439,9 @@
/** If true, hitting shift & menu will broadcast Intent.ACTION_BUG_REPORT */
boolean mEnableShiftMenuBugReports = false;
+ /** Controller that supports enabling an AccessibilityService by holding down the volume keys */
+ private AccessibilityShortcutController mAccessibilityShortcutController;
+
boolean mSafeMode;
WindowState mStatusBar = null;
int mStatusBarHeight;
@@ -748,7 +749,10 @@
private boolean mScreenshotChordVolumeDownKeyTriggered;
private long mScreenshotChordVolumeDownKeyTime;
private boolean mScreenshotChordVolumeDownKeyConsumed;
- private boolean mScreenshotChordVolumeUpKeyTriggered;
+ private boolean mA11yShortcutChordVolumeUpKeyTriggered;
+ private long mA11yShortcutChordVolumeUpKeyTime;
+ private boolean mA11yShortcutChordVolumeUpKeyConsumed;
+
private boolean mScreenshotChordPowerKeyTriggered;
private long mScreenshotChordPowerKeyTime;
@@ -794,6 +798,7 @@
private static final int MSG_BACK_LONG_PRESS = 18;
private static final int MSG_DISPOSE_INPUT_CONSUMER = 19;
private static final int MSG_BACK_DELAYED_PRESS = 20;
+ private static final int MSG_ACCESSIBILITY_SHORTCUT = 21;
private static final int MSG_REQUEST_TRANSIENT_BARS_ARG_STATUS = 0;
private static final int MSG_REQUEST_TRANSIENT_BARS_ARG_NAVIGATION = 1;
@@ -869,6 +874,9 @@
backMultiPressAction((Long) msg.obj, msg.arg1);
finishBackKeyPress();
break;
+ case MSG_ACCESSIBILITY_SHORTCUT:
+ accessibilityShortcutActivated();
+ break;
}
}
}
@@ -1213,7 +1221,7 @@
// If the power key has still not yet been handled, then detect short
// press, long press, or multi press and decide what to do.
mPowerKeyHandled = hungUp || mScreenshotChordVolumeDownKeyTriggered
- || mScreenshotChordVolumeUpKeyTriggered || gesturedServiceIntercepted;
+ || mA11yShortcutChordVolumeUpKeyTriggered || gesturedServiceIntercepted;
if (!mPowerKeyHandled) {
if (interactive) {
// When interactive, we're already awake.
@@ -1406,9 +1414,7 @@
break;
case LONG_PRESS_POWER_GLOBAL_ACTIONS:
mPowerKeyHandled = true;
- if (!performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false)) {
- performAuditoryFeedbackForAccessibilityIfNeed();
- }
+ performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false);
showGlobalActionsInternal();
break;
case LONG_PRESS_POWER_SHUT_OFF:
@@ -1439,6 +1445,10 @@
}
}
+ private void accessibilityShortcutActivated() {
+ mAccessibilityShortcutController.performAccessibilityShortcut();
+ }
+
private void disposeInputConsumer(InputConsumer inputConsumer) {
if (inputConsumer != null) {
inputConsumer.dismiss();
@@ -1484,7 +1494,7 @@
private void interceptScreenshotChord() {
if (mScreenshotChordEnabled
&& mScreenshotChordVolumeDownKeyTriggered && mScreenshotChordPowerKeyTriggered
- && !mScreenshotChordVolumeUpKeyTriggered) {
+ && !mA11yShortcutChordVolumeUpKeyTriggered) {
final long now = SystemClock.uptimeMillis();
if (now <= mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS
&& now <= mScreenshotChordPowerKeyTime
@@ -1497,6 +1507,22 @@
}
}
+ private void interceptAccessibilityShortcutChord() {
+ if (mAccessibilityShortcutController.isAccessibilityShortcutAvailable()
+ && mScreenshotChordVolumeDownKeyTriggered && mA11yShortcutChordVolumeUpKeyTriggered
+ && !mScreenshotChordPowerKeyTriggered) {
+ final long now = SystemClock.uptimeMillis();
+ if (now <= mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS
+ && now <= mA11yShortcutChordVolumeUpKeyTime
+ + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) {
+ mScreenshotChordVolumeDownKeyConsumed = true;
+ mA11yShortcutChordVolumeUpKeyConsumed = true;
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
+ ViewConfiguration.get(mContext).getAccessibilityShortcutKeyTimeout());
+ }
+ }
+ }
+
private long getScreenshotChordLongPressDelay() {
if (mKeyguardDelegate.isShowing()) {
// Double the time it takes to take a screenshot from the keyguard
@@ -1510,13 +1536,15 @@
mHandler.removeCallbacks(mScreenshotRunnable);
}
+ private void cancelPendingAccessibilityShortcutAction() {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ }
+
private final Runnable mEndCallLongPress = new Runnable() {
@Override
public void run() {
mEndCallKeyHandled = true;
- if (!performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false)) {
- performAuditoryFeedbackForAccessibilityIfNeed();
- }
+ performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false);
showGlobalActionsInternal();
}
};
@@ -1698,7 +1726,8 @@
mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mHasFeatureWatch = mContext.getPackageManager().hasSystemFeature(FEATURE_WATCH);
-
+ mAccessibilityShortcutController =
+ new AccessibilityShortcutController(mContext, new Handler());
// Init display burn-in protection
boolean burnInProtectionEnabled = context.getResources().getBoolean(
com.android.internal.R.bool.config_enableBurnInProtection);
@@ -3251,6 +3280,33 @@
}
}
+ // If an accessibility shortcut might be partially complete, hold off dispatching until we
+ // know if it is complete or not
+ if (mAccessibilityShortcutController.isAccessibilityShortcutAvailable()
+ && (flags & KeyEvent.FLAG_FALLBACK) == 0) {
+ if (mScreenshotChordVolumeDownKeyTriggered ^ mA11yShortcutChordVolumeUpKeyTriggered) {
+ final long now = SystemClock.uptimeMillis();
+ final long timeoutTime = (mScreenshotChordVolumeDownKeyTriggered
+ ? mScreenshotChordVolumeDownKeyTime : mA11yShortcutChordVolumeUpKeyTime)
+ + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS;
+ if (now < timeoutTime) {
+ return timeoutTime - now;
+ }
+ }
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && mScreenshotChordVolumeDownKeyConsumed) {
+ if (!down) {
+ mScreenshotChordVolumeDownKeyConsumed = false;
+ }
+ return -1;
+ }
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mA11yShortcutChordVolumeUpKeyConsumed) {
+ if (!down) {
+ mA11yShortcutChordVolumeUpKeyConsumed = false;
+ }
+ return -1;
+ }
+ }
+
// Cancel any pending meta actions if we see any other keys being pressed between the down
// of the meta key and its corresponding up.
if (mPendingMetaAction && !KeyEvent.isMetaKey(keyCode)) {
@@ -5760,22 +5816,32 @@
mScreenshotChordVolumeDownKeyConsumed = false;
cancelPendingPowerKeyAction();
interceptScreenshotChord();
+ if (!keyguardActive) {
+ interceptAccessibilityShortcutChord();
+ }
}
} else {
mScreenshotChordVolumeDownKeyTriggered = false;
cancelPendingScreenshotChordAction();
+ cancelPendingAccessibilityShortcutAction();
}
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
if (down) {
- if (interactive && !mScreenshotChordVolumeUpKeyTriggered
+ if (interactive && !mA11yShortcutChordVolumeUpKeyTriggered
&& (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
- mScreenshotChordVolumeUpKeyTriggered = true;
+ mA11yShortcutChordVolumeUpKeyTriggered = true;
+ mA11yShortcutChordVolumeUpKeyTime = event.getDownTime();
+ mA11yShortcutChordVolumeUpKeyConsumed = false;
cancelPendingPowerKeyAction();
cancelPendingScreenshotChordAction();
+ if (!keyguardActive) {
+ interceptAccessibilityShortcutChord();
+ }
}
} else {
- mScreenshotChordVolumeUpKeyTriggered = false;
+ mA11yShortcutChordVolumeUpKeyTriggered = false;
cancelPendingScreenshotChordAction();
+ cancelPendingAccessibilityShortcutAction();
}
}
if (down) {
@@ -5863,6 +5929,8 @@
}
case KeyEvent.KEYCODE_POWER: {
+ // Any activity on the power button stops the accessibility shortcut
+ cancelPendingAccessibilityShortcutAction();
result &= ~ACTION_PASS_TO_USER;
isWakeKey = false; // wake-up will be handled separately
if (down) {
@@ -7416,31 +7484,11 @@
}
}
- private void performAuditoryFeedbackForAccessibilityIfNeed() {
- if (!isGlobalAccessibilityGestureEnabled()) {
- return;
- }
- AudioManager audioManager = (AudioManager) mContext.getSystemService(
- Context.AUDIO_SERVICE);
- if (audioManager.isSilentMode()) {
- return;
- }
- Ringtone ringTone = RingtoneManager.getRingtone(mContext,
- Settings.System.DEFAULT_NOTIFICATION_URI);
- ringTone.setStreamType(AudioManager.STREAM_MUSIC);
- ringTone.play();
- }
-
private boolean isTheaterModeEnabled() {
return Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.THEATER_MODE_ON, 0) == 1;
}
- private boolean isGlobalAccessibilityGestureEnabled() {
- return Settings.Global.getInt(mContext.getContentResolver(),
- Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1;
- }
-
private boolean areSystemNavigationKeysEnabled() {
return Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.SYSTEM_NAVIGATION_KEYS_ENABLED, 0, UserHandle.USER_CURRENT) == 1;
diff --git a/services/tests/servicestests/src/com/android/server/policy/AccessibilityShortcutControllerTest.java b/services/tests/servicestests/src/com/android/server/policy/AccessibilityShortcutControllerTest.java
new file mode 100644
index 0000000..e2aff16
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/policy/AccessibilityShortcutControllerTest.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.provider.Settings;
+import android.support.test.runner.AndroidJUnit4;
+
+import android.test.mock.MockContentResolver;
+import android.text.TextUtils;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.IAccessibilityManager;
+import android.widget.Toast;
+import com.android.internal.R;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.server.policy.AccessibilityShortcutController.FrameworkObjectProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.internal.util.reflection.Whitebox;
+
+import java.util.Collections;
+
+import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN;
+import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+@RunWith(AndroidJUnit4.class)
+public class AccessibilityShortcutControllerTest {
+ private static final String SERVICE_NAME_STRING = "fake.package/fake.service.name";
+
+ private @Mock Context mContext;
+ private @Mock FrameworkObjectProvider mFrameworkObjectProvider;
+ private @Mock IAccessibilityManager mAccessibilityManagerService;
+ private @Mock Handler mHandler;
+ private @Mock AlertDialog.Builder mAlertDialogBuilder;
+ private @Mock AlertDialog mAlertDialog;
+ private @Mock AccessibilityServiceInfo mServiceInfo;
+ private @Mock Resources mResources;
+ private @Mock Toast mToast;
+
+ private MockContentResolver mContentResolver;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mContentResolver = new MockContentResolver(mContext);
+ mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ when(mContext.getContentResolver()).thenReturn(mContentResolver);
+ when(mContext.getResources()).thenReturn(mResources);
+
+ when(mAccessibilityManagerService.getInstalledAccessibilityServiceList(anyInt()))
+ .thenReturn(Collections.singletonList(mServiceInfo));
+
+ // Use the extra level of indirection in the object to mock framework objects
+ AccessibilityManager accessibilityManager =
+ new AccessibilityManager(mHandler, mAccessibilityManagerService, 0);
+ when(mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext))
+ .thenReturn(accessibilityManager);
+ when(mFrameworkObjectProvider.getAlertDialogBuilder(mContext))
+ .thenReturn(mAlertDialogBuilder);
+ when(mFrameworkObjectProvider.makeToastFromText(eq(mContext), anyObject(), anyInt()))
+ .thenReturn(mToast);
+
+ when(mResources.getString(anyInt())).thenReturn("Howdy %s");
+ ResolveInfo resolveInfo = mock(ResolveInfo.class);
+ when(resolveInfo.loadLabel(anyObject())).thenReturn("Service name");
+ when(mServiceInfo.getResolveInfo()).thenReturn(resolveInfo);
+ when(mServiceInfo.getComponentName())
+ .thenReturn(ComponentName.unflattenFromString(SERVICE_NAME_STRING));
+
+ when(mAlertDialogBuilder.setTitle(anyInt())).thenReturn(mAlertDialogBuilder);
+ when(mAlertDialogBuilder.setCancelable(anyBoolean())).thenReturn(mAlertDialogBuilder);
+ when(mAlertDialogBuilder.setMessage(anyObject())).thenReturn(mAlertDialogBuilder);
+ when(mAlertDialogBuilder.setPositiveButton(anyInt(), anyObject()))
+ .thenReturn(mAlertDialogBuilder);
+ when(mAlertDialogBuilder.setNegativeButton(anyInt(), anyObject()))
+ .thenReturn(mAlertDialogBuilder);
+ when(mAlertDialogBuilder.setOnCancelListener(anyObject())).thenReturn(mAlertDialogBuilder);
+ when(mAlertDialogBuilder.create()).thenReturn(mAlertDialog);
+
+ Window window = mock(Window.class);
+ Whitebox.setInternalState(window, "mWindowAttributes", new WindowManager.LayoutParams());
+ when(mAlertDialog.getWindow()).thenReturn(window);
+ }
+
+ @After
+ public void tearDown() {
+ }
+
+ @Test
+ public void testShortcutAvailable_withNullServiceIdWhenCreated_shouldReturnFalse() {
+ configureShortcutDisabled();
+ assertFalse(getController().isAccessibilityShortcutAvailable());
+ }
+
+ @Test
+ public void testShortcutAvailable_withNonNullServiceIdWhenCreated_shouldReturnTrue() {
+ configureShortcutEnabled();
+ assertTrue(getController().isAccessibilityShortcutAvailable());
+ }
+
+ @Test
+ public void testShortcutAvailable_whenServiceIdBecomesNull_shouldReturnFalse() {
+ configureShortcutEnabled();
+ AccessibilityShortcutController accessibilityShortcutController = getController();
+ Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "");
+ accessibilityShortcutController.onSettingsChanged();
+ assertFalse(accessibilityShortcutController.isAccessibilityShortcutAvailable());
+ }
+
+ @Test
+ public void testShortcutAvailable_whenServiceIdBecomesNonNull_shouldReturnTrue() {
+ configureShortcutDisabled();
+ AccessibilityShortcutController accessibilityShortcutController = getController();
+ configureShortcutEnabled();
+ accessibilityShortcutController.onSettingsChanged();
+ assertTrue(accessibilityShortcutController.isAccessibilityShortcutAvailable());
+ }
+
+ @Test
+ public void testOnAccessibilityShortcut_firstTime_showsWarningDialog()
+ throws Exception {
+ configureShortcutEnabled();
+ AccessibilityShortcutController accessibilityShortcutController = getController();
+ Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0);
+ accessibilityShortcutController.performAccessibilityShortcut();
+
+ assertEquals(1, Settings.Secure.getInt(
+ mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0));
+ verify(mResources).getString(R.string.accessibility_shortcut_toogle_warning);
+ verify(mAlertDialog).show();
+ verify(mAccessibilityManagerService).getInstalledAccessibilityServiceList(anyInt());
+ verify(mAccessibilityManagerService, times(0)).performAccessibilityShortcut();
+ }
+
+ @Test
+ public void testOnAccessibilityShortcut_withDialogShowing_callsServer()
+ throws Exception {
+ configureShortcutEnabled();
+ AccessibilityShortcutController accessibilityShortcutController = getController();
+ Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0);
+ accessibilityShortcutController.performAccessibilityShortcut();
+ accessibilityShortcutController.performAccessibilityShortcut();
+ verify(mToast).show();
+ verify(mAccessibilityManagerService, times(1)).performAccessibilityShortcut();
+ }
+
+ @Test
+ public void testOnAccessibilityShortcut_ifCanceledFirstTime_showsWarningDialog()
+ throws Exception {
+ configureShortcutEnabled();
+ AccessibilityShortcutController accessibilityShortcutController = getController();
+ Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0);
+ accessibilityShortcutController.performAccessibilityShortcut();
+ ArgumentCaptor<AlertDialog.OnCancelListener> cancelListenerCaptor =
+ ArgumentCaptor.forClass(AlertDialog.OnCancelListener.class);
+ verify(mAlertDialogBuilder).setOnCancelListener(cancelListenerCaptor.capture());
+ // Call the cancel callback
+ cancelListenerCaptor.getValue().onCancel(null);
+
+ accessibilityShortcutController.performAccessibilityShortcut();
+ verify(mAlertDialog, times(2)).show();
+ }
+
+ @Test
+ public void testClickingDisableButtonInDialog_shouldClearShortcutId() {
+ configureShortcutEnabled();
+ Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0);
+ getController().performAccessibilityShortcut();
+
+ ArgumentCaptor<DialogInterface.OnClickListener> captor =
+ ArgumentCaptor.forClass(DialogInterface.OnClickListener.class);
+ verify(mAlertDialogBuilder).setNegativeButton(eq(R.string.disable_accessibility_shortcut),
+ captor.capture());
+ // Call the button callback
+ captor.getValue().onClick(null, 0);
+ assertTrue(TextUtils.isEmpty(
+ Settings.Secure.getString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)));
+ }
+
+ @Test
+ public void testClickingLeaveOnButtonInDialog_shouldLeaveShortcutReady() throws Exception {
+ configureShortcutEnabled();
+ Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0);
+ getController().performAccessibilityShortcut();
+
+ ArgumentCaptor<DialogInterface.OnClickListener> captor =
+ ArgumentCaptor.forClass(DialogInterface.OnClickListener.class);
+ verify(mAlertDialogBuilder).setPositiveButton(eq(R.string.leave_accessibility_shortcut_on),
+ captor.capture());
+ // Call the button callback, if one exists
+ if (captor.getValue() != null) {
+ captor.getValue().onClick(null, 0);
+ }
+ assertEquals(SERVICE_NAME_STRING,
+ Settings.Secure.getString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE));
+ assertEquals(1, Settings.Secure.getInt(
+ mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN));
+ }
+
+ @Test
+ public void testOnAccessibilityShortcut_afterDialogShown_shouldCallServer() throws Exception {
+ configureShortcutEnabled();
+ Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1);
+ getController().performAccessibilityShortcut();
+
+ verifyZeroInteractions(mAlertDialogBuilder, mAlertDialog);
+ verify(mToast).show();
+ verify(mAccessibilityManagerService).performAccessibilityShortcut();
+ }
+
+ private void configureShortcutDisabled() {
+ Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "");
+ }
+
+ private void configureShortcutEnabled() {
+ Settings.Secure.putString(
+ mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, SERVICE_NAME_STRING);
+ }
+
+ private AccessibilityShortcutController getController() {
+ AccessibilityShortcutController accessibilityShortcutController =
+ new AccessibilityShortcutController(mContext, mHandler);
+ accessibilityShortcutController.mFrameworkObjectProvider = mFrameworkObjectProvider;
+ return accessibilityShortcutController;
+ }
+}