diff options
author | 2016-11-22 18:18:39 -0800 | |
---|---|---|
committer | 2017-01-18 17:12:54 -0800 | |
commit | 106fe732050f3d75a08c3bc48fdbcf84cac20b41 (patch) | |
tree | 6134b208951b078d6a09155cf03c07c5c238e24c | |
parent | 0e39a438bfa5c279d361cf59e745df1d326c57ba (diff) |
New accessibility shortcut.
Removing accessibility gesture from power dialog.
Adding new accessibility shortcut activated by holding both volume
buttons down. This shortcut is configurable by OEMs and users to
work with any installed accessibility service.
Bug: 30160335
Test: Added automated testing for the EnableAccessibilityController.
Manually toggled various services on and off.
Change-Id: I546bd29a2ab1ba64a0cbfd11e2004cdf85ee6cfd
15 files changed, 865 insertions, 456 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index 07a8253aa9b6..b76aeb71101a 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -414,9 +414,9 @@ public class AccessibilityServiceInfo implements Parcelable { 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 class AccessibilityServiceInfo implements Parcelable { 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 @@ public class AccessibilityServiceInfo implements Parcelable { * @hide */ public void setComponentName(ComponentName component) { - mId = component.flattenToShortString(); + mComponentName = component; + } + + /** + * @hide + */ + public ComponentName getComponentName() { + return mComponentName; } /** @@ -585,7 +592,7 @@ public class AccessibilityServiceInfo implements Parcelable { * @return The id. */ public String getId() { - return mId; + return mComponentName.flattenToShortString(); } /** @@ -715,7 +722,7 @@ public class AccessibilityServiceInfo implements Parcelable { 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 @@ public class AccessibilityServiceInfo implements Parcelable { 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 @@ public class AccessibilityServiceInfo implements Parcelable { @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 @@ public class AccessibilityServiceInfo implements Parcelable { 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 @@ public class AccessibilityServiceInfo implements Parcelable { 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 893e53c81d61..371c0f3b90ca 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5313,6 +5313,21 @@ public final class Settings { 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 @@ public final class Settings { 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 @@ public final class Settings { * 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 @@ public final class Settings { 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 6d2f850b94f4..0e753f39535d 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -83,6 +83,13 @@ public class ViewConfiguration { 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 @@ public class ViewConfiguration { } /** + * 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 7f940f1717f4..bfb8d8396665 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -21,6 +21,7 @@ import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ 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 @@ public final class AccessibilityManager { * @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 @@ public final class AccessibilityManager { } /** + * 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 @@ public final class AccessibilityManager { } /** + * 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 @@ public final class AccessibilityManager { } } + /** + * 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 282974430891..ed77f68f2195 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -60,7 +60,6 @@ interface IAccessibilityManager { 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 db157bf9cbf3..7de48d3305bb 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 87a47326c8ee..0204e9359c63 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 0a757442d969..c370ef74f6a9 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 fcff305e2daa..9bb3c36056a9 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.text.TextUtils; 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 @@ public class AccessibilityUtils { 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 b34e4e40a49e..ece51494ef32 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.content.PackageMonitor; 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { mInitialized = true; updateLegacyCapabilitiesLocked(userState); updateServicesLocked(userState); + updateAccessibilityShortcutLocked(userState); updateWindowsForAccessibilityCallbackLocked(userState); updateAccessibilityFocusBehaviorLocked(userState); updateFilterKeyEventsLocked(userState); @@ -1613,7 +1615,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { somethingChanged |= readEnhancedWebAccessibilityEnabledChangedLocked(userState); somethingChanged |= readDisplayMagnificationEnabledSettingLocked(userState); somethingChanged |= readAutoclickEnabledSettingLocked(userState); - + somethingChanged |= readAccessibilityShortcutSettingLocked(userState); return somethingChanged; } @@ -1722,6 +1724,50 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } + 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } /** - * Enables accessibility service specified by {@param componentName} for the {@param userId}. + * 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 enableAccessibilityService(ComponentName componentName, int userId) { + 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) { - if (Binder.getCallingUid() != Process.SYSTEM_UID) { - throw new SecurityException("only SYSTEM can call enableAccessibilityService."); + UserState userState = getUserStateLocked(mCurrentUserId); + ComponentName serviceName = userState.mServiceToEnableWithShortcut; + if (serviceName == null) { + return; } - - 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); + 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}. + */ + private void enableAccessibilityServiceLocked(ComponentName componentName, int userId) { + 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); + } } /** * 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"); - } - - 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); - } + private void disableAccessibilityServiceLocked(ComponentName componentName, int userId) { + 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); } } @@ -4307,6 +4372,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { public ComponentName mServiceChangingSoftKeyboardMode; + public ComponentName mServiceToEnableWithShortcut; + public int mLastSentClientState = -1; public int mSoftKeyboardShowMode = 0; @@ -4439,6 +4506,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { 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 000000000000..133881aaaaf7 --- /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 6b203a910a25..000000000000 --- 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 d4adcc4b8383..335a2309bdce 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.Handler; 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.text.TextUtils; 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 @@ class GlobalActions implements DialogInterface.OnDismissListener, DialogInterfac 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 @@ class GlobalActions implements DialogInterface.OnDismissListener, DialogInterfac @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 4b2b184d1bbc..32b8c9bdd3f6 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.AudioAttributes; 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { /** 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { backMultiPressAction((Long) msg.obj, msg.arg1); finishBackKeyPress(); break; + case MSG_ACCESSIBILITY_SHORTCUT: + accessibilityShortcutActivated(); + break; } } } @@ -1213,7 +1221,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { // 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + private void accessibilityShortcutActivated() { + mAccessibilityShortcutController.performAccessibilityShortcut(); + } + private void disposeInputConsumer(InputConsumer inputConsumer) { if (inputConsumer != null) { inputConsumer.dismiss(); @@ -1484,7 +1494,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + // 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { } 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - 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 000000000000..e2aff1610e89 --- /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; + } +} |