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); }