From 7befb7deb2ac15134b3bb190520cba19165d16dd Mon Sep 17 00:00:00 2001 From: Svetoslav Ganov Date: Thu, 27 Sep 2012 16:49:23 -0700 Subject: [PATCH] Global gesture to toggle Accessibility system-wide. 1. This change adds a global gesture for enabling accessibility. To enable this gesture the user has to allow it from the accessibility settings or use the setup wizard to enable accessibility. When the global gesture is enabled the user can long press on power to bring the global actions dialog and then hold with two fingers for a few seconds to enable accessibility. The appropriate feedback is also provided. 2. The global gesture is writing directly into the settings for the current user if performed when the keyguard is not on. If the keygaurd is on and the current user has no accessibility enabled, the gesture will temporary enable accessibility for the current user, i.e. no settings are changed, to allow the blind user to log into his account. As soon as a user switch happens the new user settings are inherited. If no user change happens after temporary enabling accessibility the temporary changes will be undone when the keyguard goes away and the device will works as expected by the current user. bug:6171929 3. The initialization code for the owner was not executed due to a redundant check, thus putting the accessibility layer in an inconsistent state which breaks pretty much everything. bug:7240414 Change-Id: Ie7d7aba80f5867b7f88d5893b848b53fb02a7537 --- core/java/android/provider/Settings.java | 15 +- .../accessibility/IAccessibilityManager.aidl | 4 + core/res/AndroidManifest.xml | 6 + core/res/res/values/dimens.xml | 3 + core/res/res/values/strings.xml | 16 + core/res/res/values/symbols.xml | 5 + .../impl/EnableAccessibilityController.java | 277 ++++++++++++++++++ .../internal/policy/impl/GlobalActions.java | 146 ++++++++- .../policy/impl/PhoneWindowManager.java | 25 +- .../AccessibilityManagerService.java | 147 +++++++++- 10 files changed, 621 insertions(+), 23 deletions(-) create mode 100644 policy/src/com/android/internal/policy/impl/EnableAccessibilityController.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ffc48d862c492..550713ddc15fd 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3300,7 +3300,7 @@ public final class Settings { "enabled_accessibility_services"; /** - * List of the accessibility services to which the user has graned + * List of the accessibility services to which the user has granted * permission to put the device into touch exploration mode. * * @hide @@ -3319,7 +3319,7 @@ public final class Settings { *

* Note: The JavaScript based screen-reader is served by the * Google infrastructure and enable users with disabilities to - * efficiantly navigate in and explore web content. + * efficiently navigate in and explore web content. *

*

* This property represents a boolean value. @@ -3331,7 +3331,7 @@ public final class Settings { /** * The URL for the injected JavaScript based screen-reader used - * for providing accessiblity of content in WebView. + * for providing accessibility of content in WebView. *

* Note: The JavaScript based screen-reader is served by the * Google infrastructure and enable users with disabilities to @@ -4109,6 +4109,15 @@ public final class Settings { */ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/global"); + /** + * 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 + */ + public static final String ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED = + "enable_accessibility_global_gesture_enabled"; + /** * Whether Airplane Mode is on. */ diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 60238627ae52c..c3ef54c6409f8 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -20,6 +20,7 @@ package android.view.accessibility; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceConnection; import android.accessibilityservice.IAccessibilityServiceClient; +import android.content.ComponentName; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnection; @@ -53,4 +54,7 @@ interface IAccessibilityManager { in AccessibilityServiceInfo info); void unregisterUiTestAutomationService(IAccessibilityServiceClient client); + + void temporaryEnableAccessibilityStateUntilKeyguardRemoved(in ComponentName service, + boolean touchExplorationEnabled); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index a8bee4dc1500c..8dbaa2604a8aa 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1578,6 +1578,12 @@ android:description="@string/permdesc_retrieve_window_info" android:protectionLevel="signature" /> + + + 46dp + + 80dip + diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index c90f4f27028d5..02aa537604c70 100755 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -743,6 +743,13 @@ the content of the active window. Malicious apps may retrieve the entire window content and examine all its text except passwords. + + temporary enable accessibility + + Allows an application to temporarily + enable accessibility on the device. Malicious apps may enable accessibility without + user consent. + retrieve window info @@ -3903,4 +3910,13 @@ + + Continue touching the screen to enable accessibility. + + Accessibility enabled. + + Enable accessibility canceled. + + Switched to user %1$s. + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index d85e58120ea8c..9a4136b2a29c1 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -290,6 +290,7 @@ + @@ -357,6 +358,7 @@ + @@ -437,6 +439,7 @@ + @@ -470,6 +473,7 @@ + @@ -778,6 +782,7 @@ + diff --git a/policy/src/com/android/internal/policy/impl/EnableAccessibilityController.java b/policy/src/com/android/internal/policy/impl/EnableAccessibilityController.java new file mode 100644 index 0000000000000..889463b7eec0a --- /dev/null +++ b/policy/src/com/android/internal/policy/impl/EnableAccessibilityController.java @@ -0,0 +1,277 @@ +/* + * 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.internal.policy.impl; + +import android.accessibilityservice.AccessibilityServiceInfo; +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.MathUtils; +import android.view.IWindowManager; +import android.view.MotionEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.IAccessibilityManager; + +import com.android.internal.R; + +import java.util.Iterator; +import java.util.List; + +public class 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 IWindowManager mWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService("window")); + + private final IAccessibilityManager mAccessibilityManager = IAccessibilityManager + .Stub.asInterface(ServiceManager.getService("accessibility")); + + + private final Context mContext; + 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) { + mContext = context; + 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(); + } + + private static List getInstalledSpeakingAccessibilityServices( + Context context) { + List services = AccessibilityManager.getInstance( + context).getInstalledAccessibilityServiceList(); + Iterator 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() { + List services = getInstalledSpeakingAccessibilityServices( + mContext); + if (services.isEmpty()) { + return; + } + boolean keyguardLocked = false; + try { + keyguardLocked = mWindowManager.isKeyguardLocked(); + } catch (RemoteException re) { + /* ignore */ + } + + final boolean hasMoreThanOneUser = mUserManager.getUsers().size() > 1; + + AccessibilityServiceInfo service = services.get(0); + boolean enableTouchExploration = (service.flags + & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0; + // Try to find a service supporting explore by touch. + if (!enableTouchExploration) { + final int serviceCount = services.size(); + for (int i = 1; i < serviceCount; i++) { + AccessibilityServiceInfo candidate = services.get(i); + if ((candidate.flags & AccessibilityServiceInfo + .FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0) { + enableTouchExploration = true; + service = candidate; + break; + } + } + } + + ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; + ComponentName componentName = new ComponentName(serviceInfo.packageName, serviceInfo.name); + if (!keyguardLocked || !hasMoreThanOneUser) { + final int userId = ActivityManager.getCurrentUser(); + String enabledServiceString = componentName.flattenToString(); + ContentResolver resolver = mContext.getContentResolver(); + // Enable one speaking accessibility service. + Settings.Secure.putStringForUser(resolver, + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + enabledServiceString, userId); + // Allow the services we just enabled to toggle touch exploration. + Settings.Secure.putStringForUser(resolver, + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + enabledServiceString, userId); + // Enable touch exploration. + if (enableTouchExploration) { + Settings.Secure.putIntForUser(resolver, Settings.Secure.TOUCH_EXPLORATION_ENABLED, + 1, userId); + } + // Enable accessibility script injection (AndroidVox) for web content. + Settings.Secure.putIntForUser(resolver, Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, + 1, userId); + // Turn on accessibility mode last. + Settings.Secure.putIntForUser(resolver, Settings.Secure.ACCESSIBILITY_ENABLED, + 1, userId); + } else if (keyguardLocked) { + try { + mAccessibilityManager.temporaryEnableAccessibilityStateUntilKeyguardRemoved( + componentName, enableTouchExploration); + } catch (RemoteException re) { + /* ignore */ + } + } + } +} diff --git a/policy/src/com/android/internal/policy/impl/GlobalActions.java b/policy/src/com/android/internal/policy/impl/GlobalActions.java index d8e361fbcc50d..0f9ad59678294 100644 --- a/policy/src/com/android/internal/policy/impl/GlobalActions.java +++ b/policy/src/com/android/internal/policy/impl/GlobalActions.java @@ -16,12 +16,15 @@ package com.android.internal.policy.impl; +import com.android.internal.app.AlertController; +import com.android.internal.app.AlertController.AlertParams; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.telephony.TelephonyProperties; import com.android.internal.R; import android.app.ActivityManagerNative; import android.app.AlertDialog; +import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -32,11 +35,11 @@ import android.database.ContentObserver; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.net.ConnectivityManager; +import android.os.Bundle; import android.os.Handler; -import android.os.IBinder; 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; @@ -46,17 +49,21 @@ import android.telephony.PhoneStateListener; import android.telephony.ServiceState; import android.telephony.TelephonyManager; import android.util.Log; -import android.view.IWindowManager; +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; import android.view.WindowManagerPolicy.WindowManagerFuncs; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ImageView.ScaleType; +import android.widget.ListView; import android.widget.TextView; import java.util.ArrayList; @@ -78,7 +85,7 @@ class GlobalActions implements DialogInterface.OnDismissListener, DialogInterfac private final AudioManager mAudioManager; private ArrayList mItems; - private AlertDialog mDialog; + private GlobalActionsDialog mDialog; private Action mSilentModeAction; private ToggleAction mAirplaneModeOn; @@ -150,7 +157,7 @@ class GlobalActions implements DialogInterface.OnDismissListener, DialogInterfac * Create the global actions dialog. * @return A new dialog. */ - private AlertDialog createDialog() { + private GlobalActionsDialog createDialog() { // Simple toggle style if there's no vibrator, otherwise use a tri-state if (!mHasVibrator) { mSilentModeAction = new SilentModeToggleAction(); @@ -319,12 +326,14 @@ class GlobalActions implements DialogInterface.OnDismissListener, DialogInterfac mAdapter = new MyAdapter(); - final AlertDialog.Builder ab = new AlertDialog.Builder(mContext); + AlertParams params = new AlertParams(mContext); + params.mAdapter = mAdapter; + params.mOnClickListener = this; + params.mForceInverseBackground = true; - ab.setAdapter(mAdapter, this) - .setInverseBackgroundForced(true); + GlobalActionsDialog dialog = new GlobalActionsDialog(mContext, params); + dialog.setCanceledOnTouchOutside(false); // Handled by the custom class. - final AlertDialog dialog = ab.create(); dialog.getListView().setItemsCanFocus(true); dialog.getListView().setLongClickable(true); dialog.getListView().setOnItemLongClickListener( @@ -872,4 +881,121 @@ class GlobalActions implements DialogInterface.OnDismissListener, DialogInterfac mAirplaneState = on ? ToggleAction.State.On : ToggleAction.State.Off; } } + + private static final class GlobalActionsDialog extends Dialog implements DialogInterface { + private final Context mContext; + private final int mWindowTouchSlop; + private final AlertController mAlert; + + private EnableAccessibilityController mEnableAccessibilityController; + + private boolean mIntercepted; + private boolean mCancelOnUp; + + public GlobalActionsDialog(Context context, AlertParams params) { + super(context, getDialogTheme(context)); + mContext = context; + mAlert = new AlertController(mContext, this, getWindow()); + mWindowTouchSlop = ViewConfiguration.get(context).getScaledWindowTouchSlop(); + params.apply(mAlert); + } + + private static int getDialogTheme(Context context) { + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(com.android.internal.R.attr.alertDialogTheme, + outValue, true); + return outValue.resourceId; + } + + @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); + super.setCanceledOnTouchOutside(false); + } else { + mEnableAccessibilityController = null; + 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(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAlert.installContent(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mAlert.onKeyDown(keyCode, event)) { + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (mAlert.onKeyUp(keyCode, event)) { + return true; + } + return super.onKeyUp(keyCode, event); + } + } } diff --git a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java index 13ad285e65dc6..e37075f91d1a0 100755 --- a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java +++ b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java @@ -40,6 +40,8 @@ import android.graphics.PixelFormat; import android.graphics.Rect; import android.media.AudioManager; import android.media.IAudioService; +import android.media.Ringtone; +import android.media.RingtoneManager; import android.os.Bundle; import android.os.FactoryTest; import android.os.Handler; @@ -747,7 +749,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; case LONG_PRESS_POWER_GLOBAL_ACTIONS: mPowerKeyHandled = true; - performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false); + if (!performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false)) { + performAuditoryFeedbackForAccessibilityIfNeed(); + } sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS); showGlobalActionsDialog(); break; @@ -4250,6 +4254,25 @@ 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 isGlobalAccessibilityGestureEnabled() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1; + } + public boolean performHapticFeedbackLw(WindowState win, int effectId, boolean always) { final boolean hapticsDisabled = Settings.System.getIntForUser(mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 0, UserHandle.USER_CURRENT) == 0; diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java index 25f98de03f081..cae67e9aff7ab 100644 --- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -56,6 +56,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings; import android.text.TextUtils; import android.text.TextUtils.SimpleStringSplitter; @@ -108,9 +109,16 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { private static final String LOG_TAG = "AccessibilityManagerService"; + // TODO: This is arbitrary. When there is time implement this by watching + // when that accessibility services are bound. + private static final int WAIT_FOR_USER_STATE_FULLY_INITIALIZED_MILLIS = 5000; + private static final String FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE = "registerUiTestAutomationService"; + private static final String TEMPORARY_ENABLE_ACCESSIBILITY_UNTIL_KEYGUARD_REMOVED = + "temporaryEnableAccessibilityStateUntilKeyguardRemoved"; + private static final char COMPONENT_NAME_SEPARATOR = ':'; private static final int OWN_PROCESS_ID = android.os.Process.myPid(); @@ -157,6 +165,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { private final SparseArray mUserStates = new SparseArray(); + private final TempUserStateChangeMemento mTempStateChangeForCurrentUserMemento = + new TempUserStateChangeMemento(); + private int mCurrentUserId = UserHandle.USER_OWNER; private UserState getCurrentUserStateLocked() { @@ -268,12 +279,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { // package changes monitor.register(mContext, null, UserHandle.ALL, true); - // user change - IntentFilter userFilter = new IntentFilter(); - userFilter.addAction(Intent.ACTION_USER_SWITCHED); - userFilter.addAction(Intent.ACTION_USER_REMOVED); + // user change and unlock + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_USER_SWITCHED); + intentFilter.addAction(Intent.ACTION_USER_REMOVED); + intentFilter.addAction(Intent.ACTION_USER_PRESENT); - mContext.registerReceiver(new BroadcastReceiver() { + mContext.registerReceiverAsUser(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); @@ -281,9 +293,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); } else if (Intent.ACTION_USER_REMOVED.equals(action)) { removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + } else if (Intent.ACTION_USER_PRESENT.equals(action)) { + restoreStateFromMementoIfNeeded(); } } - }, userFilter); + }, UserHandle.ALL, intentFilter, null, null); } public int addClient(IAccessibilityManagerClient client, int userId) { @@ -510,6 +524,37 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } + public void temporaryEnableAccessibilityStateUntilKeyguardRemoved( + ComponentName service, boolean touchExplorationEnabled) { + mSecurityPolicy.enforceCallingPermission( + Manifest.permission.TEMPORARY_ENABLE_ACCESSIBILITY, + TEMPORARY_ENABLE_ACCESSIBILITY_UNTIL_KEYGUARD_REMOVED); + try { + if (!mWindowManagerService.isKeyguardLocked()) { + return; + } + } catch (RemoteException re) { + return; + } + synchronized (mLock) { + UserState userState = getCurrentUserStateLocked(); + // Stash the old state so we can restore it when the keyguard is gone. + mTempStateChangeForCurrentUserMemento.initialize(mCurrentUserId, getCurrentUserStateLocked()); + // Set the temporary state. + userState.mIsAccessibilityEnabled = true; + userState.mIsTouchExplorationEnabled= touchExplorationEnabled; + userState.mIsDisplayMagnificationEnabled = false; + userState.mEnabledServices.clear(); + userState.mEnabledServices.add(service); + userState.mTouchExplorationGrantedServices.clear(); + userState.mTouchExplorationGrantedServices.add(service); + // Update the internal state. + performServiceManagementLocked(userState); + updateInputFilterLocked(userState); + scheduleSendStateToClientsLocked(userState); + } + } + public void unregisterUiTestAutomationService(IAccessibilityServiceClient serviceClient) { synchronized (mLock) { // Automation service is not bound, so pretend it died to perform clean up. @@ -600,9 +645,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { private void switchUser(int userId) { synchronized (mLock) { - if (userId == mCurrentUserId) { - return; - } + // The user switched so we do not need to restore the current user + // state since we will fully rebuild it when he becomes current again. + mTempStateChangeForCurrentUserMemento.clear(); // Disconnect from services for the old user. UserState oldUserState = getUserStateLocked(mCurrentUserId); @@ -620,6 +665,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { // Recreate the internal state for the new user. mMainHandler.obtainMessage(MainHandler.MSG_SEND_RECREATE_INTERNAL_STATE, mCurrentUserId, 0).sendToTarget(); + + // Schedule announcement of the current user if needed. + mMainHandler.sendEmptyMessageDelayed(MainHandler.MSG_ANNOUNCE_NEW_USER_IF_NEEDED, + WAIT_FOR_USER_STATE_FULLY_INITIALIZED_MILLIS); } } @@ -629,6 +678,21 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } + private void restoreStateFromMementoIfNeeded() { + synchronized (mLock) { + if (mTempStateChangeForCurrentUserMemento.mUserId != UserHandle.USER_NULL) { + UserState userState = getCurrentUserStateLocked(); + // Restore the state from the memento. + mTempStateChangeForCurrentUserMemento.applyTo(userState); + mTempStateChangeForCurrentUserMemento.clear(); + // Update the internal state. + performServiceManagementLocked(userState); + updateInputFilterLocked(userState); + scheduleSendStateToClientsLocked(userState); + } + } + } + private Service getQueryBridge() { if (mQueryBridge == null) { AccessibilityServiceInfo info = new AccessibilityServiceInfo(); @@ -1076,6 +1140,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { handleDisplayMagnificationEnabledSettingChangedLocked(userState); handleAccessibilityEnabledSettingChangedLocked(userState); + performServiceManagementLocked(userState); updateInputFilterLocked(userState); scheduleSendStateToClientsLocked(userState); } @@ -1084,6 +1149,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { userState.mIsAccessibilityEnabled = Settings.Secure.getIntForUser( mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED, 0, userState.mUserId) == 1; + } + + private void performServiceManagementLocked(UserState userState) { if (userState.mIsAccessibilityEnabled ) { manageServicesLocked(userState); } else { @@ -1186,6 +1254,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { public static final int MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER = 3; public static final int MSG_SEND_RECREATE_INTERNAL_STATE = 4; public static final int MSG_UPDATE_ACTIVE_WINDOW = 5; + public static final int MSG_ANNOUNCE_NEW_USER_IF_NEEDED = 6; public MainHandler(Looper looper) { super(looper); @@ -1226,6 +1295,25 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { final int eventType = msg.arg2; mSecurityPolicy.updateActiveWindow(windowId, eventType); } break; + case MSG_ANNOUNCE_NEW_USER_IF_NEEDED: { + announceNewUserIfNeeded(); + } break; + } + } + + private void announceNewUserIfNeeded() { + synchronized (mLock) { + UserState userState = getCurrentUserStateLocked(); + if (userState.mIsAccessibilityEnabled) { + UserManager userManager = (UserManager) mContext.getSystemService( + Context.USER_SERVICE); + String message = mContext.getString(R.string.user_switched, + userManager.getUserInfo(mCurrentUserId).name); + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_ANNOUNCEMENT); + event.getText().add(message); + sendAccessibilityEvent(event, mCurrentUserId); + } } } @@ -2229,6 +2317,46 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { } } + private class TempUserStateChangeMemento { + public int mUserId = UserHandle.USER_NULL; + public boolean mIsAccessibilityEnabled; + public boolean mIsTouchExplorationEnabled; + public boolean mIsDisplayMagnificationEnabled; + public final Set mEnabledServices = new HashSet(); + public final Set mTouchExplorationGrantedServices = + new HashSet(); + + public void initialize(int userId, UserState userState) { + mUserId = userId; + mIsAccessibilityEnabled = userState.mIsAccessibilityEnabled; + mIsTouchExplorationEnabled = userState.mIsTouchExplorationEnabled; + mIsDisplayMagnificationEnabled = userState.mIsDisplayMagnificationEnabled; + mEnabledServices.clear(); + mEnabledServices.addAll(userState.mEnabledServices); + mTouchExplorationGrantedServices.clear(); + mTouchExplorationGrantedServices.addAll(userState.mTouchExplorationGrantedServices); + } + + public void applyTo(UserState userState) { + userState.mIsAccessibilityEnabled = mIsAccessibilityEnabled; + userState.mIsTouchExplorationEnabled = mIsTouchExplorationEnabled; + userState.mIsDisplayMagnificationEnabled = mIsDisplayMagnificationEnabled; + userState.mEnabledServices.clear(); + userState.mEnabledServices.addAll(mEnabledServices); + userState.mTouchExplorationGrantedServices.clear(); + userState.mTouchExplorationGrantedServices.addAll(mTouchExplorationGrantedServices); + } + + public void clear() { + mUserId = UserHandle.USER_NULL; + mIsAccessibilityEnabled = false; + mIsTouchExplorationEnabled = false; + mIsDisplayMagnificationEnabled = false; + mEnabledServices.clear(); + mTouchExplorationGrantedServices.clear(); + } + } + private final class AccessibilityContentObserver extends ContentObserver { private final Uri mAccessibilityEnabledUri = Settings.Secure.getUriFor( @@ -2272,6 +2400,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub { if (mUiAutomationService == null) { UserState userState = getCurrentUserStateLocked(); handleAccessibilityEnabledSettingChangedLocked(userState); + performServiceManagementLocked(userState); updateInputFilterLocked(userState); scheduleSendStateToClientsLocked(userState); }