1 /* 2 * Copyright (C) 2012 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.internal.policy.impl; 18 19 import android.accessibilityservice.AccessibilityServiceInfo; 20 import android.app.ActivityManager; 21 import android.content.ComponentName; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.pm.ServiceInfo; 25 import android.media.AudioManager; 26 import android.media.Ringtone; 27 import android.media.RingtoneManager; 28 import android.os.Handler; 29 import android.os.Message; 30 import android.os.RemoteException; 31 import android.os.ServiceManager; 32 import android.os.UserManager; 33 import android.provider.Settings; 34 import android.speech.tts.TextToSpeech; 35 import android.util.MathUtils; 36 import android.view.IWindowManager; 37 import android.view.MotionEvent; 38 import android.view.accessibility.AccessibilityManager; 39 import android.view.accessibility.IAccessibilityManager; 40 41 import com.android.internal.R; 42 43 import java.util.ArrayList; 44 import java.util.Iterator; 45 import java.util.List; 46 47 public class EnableAccessibilityController { 48 49 private static final int SPEAK_WARNING_DELAY_MILLIS = 2000; 50 private static final int ENABLE_ACCESSIBILITY_DELAY_MILLIS = 6000; 51 52 public static final int MESSAGE_SPEAK_WARNING = 1; 53 public static final int MESSAGE_SPEAK_ENABLE_CANCELED = 2; 54 public static final int MESSAGE_ENABLE_ACCESSIBILITY = 3; 55 56 private final Handler mHandler = new Handler() { 57 @Override 58 public void handleMessage(Message message) { 59 switch (message.what) { 60 case MESSAGE_SPEAK_WARNING: { 61 String text = mContext.getString(R.string.continue_to_enable_accessibility); 62 mTts.speak(text, TextToSpeech.QUEUE_FLUSH, null); 63 } break; 64 case MESSAGE_SPEAK_ENABLE_CANCELED: { 65 String text = mContext.getString(R.string.enable_accessibility_canceled); 66 mTts.speak(text, TextToSpeech.QUEUE_FLUSH, null); 67 } break; 68 case MESSAGE_ENABLE_ACCESSIBILITY: { 69 enableAccessibility(); 70 mTone.play(); 71 mTts.speak(mContext.getString(R.string.accessibility_enabled), 72 TextToSpeech.QUEUE_FLUSH, null); 73 } break; 74 } 75 } 76 }; 77 78 private final IWindowManager mWindowManager = IWindowManager.Stub.asInterface( 79 ServiceManager.getService("window")); 80 81 private final IAccessibilityManager mAccessibilityManager = IAccessibilityManager 82 .Stub.asInterface(ServiceManager.getService("accessibility")); 83 84 85 private final Context mContext; 86 private final Runnable mOnAccessibilityEnabledCallback; 87 private final UserManager mUserManager; 88 private final TextToSpeech mTts; 89 private final Ringtone mTone; 90 91 private final float mTouchSlop; 92 93 private boolean mDestroyed; 94 private boolean mCanceled; 95 96 private float mFirstPointerDownX; 97 private float mFirstPointerDownY; 98 private float mSecondPointerDownX; 99 private float mSecondPointerDownY; 100 101 public EnableAccessibilityController(Context context, Runnable onAccessibilityEnabledCallback) { 102 mContext = context; 103 mOnAccessibilityEnabledCallback = onAccessibilityEnabledCallback; 104 mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); 105 mTts = new TextToSpeech(context, new TextToSpeech.OnInitListener() { 106 @Override 107 public void onInit(int status) { 108 if (mDestroyed) { 109 mTts.shutdown(); 110 } 111 } 112 }); 113 mTone = RingtoneManager.getRingtone(context, Settings.System.DEFAULT_NOTIFICATION_URI); 114 mTone.setStreamType(AudioManager.STREAM_MUSIC); 115 mTouchSlop = context.getResources().getDimensionPixelSize( 116 R.dimen.accessibility_touch_slop); 117 } 118 119 public static boolean canEnableAccessibilityViaGesture(Context context) { 120 AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(context); 121 // Accessibility is enabled and there is an enabled speaking 122 // accessibility service, then we have nothing to do. 123 if (accessibilityManager.isEnabled() 124 && !accessibilityManager.getEnabledAccessibilityServiceList( 125 AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty()) { 126 return false; 127 } 128 // If the global gesture is enabled and there is a speaking service 129 // installed we are good to go, otherwise there is nothing to do. 130 return Settings.Global.getInt(context.getContentResolver(), 131 Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1 132 && !getInstalledSpeakingAccessibilityServices(context).isEmpty(); 133 } 134 135 private static List<AccessibilityServiceInfo> getInstalledSpeakingAccessibilityServices( 136 Context context) { 137 List<AccessibilityServiceInfo> services = new ArrayList<AccessibilityServiceInfo>(); 138 services.addAll(AccessibilityManager.getInstance(context) 139 .getInstalledAccessibilityServiceList()); 140 Iterator<AccessibilityServiceInfo> iterator = services.iterator(); 141 while (iterator.hasNext()) { 142 AccessibilityServiceInfo service = iterator.next(); 143 if ((service.feedbackType & AccessibilityServiceInfo.FEEDBACK_SPOKEN) == 0) { 144 iterator.remove(); 145 } 146 } 147 return services; 148 } 149 150 public void onDestroy() { 151 mDestroyed = true; 152 } 153 154 public boolean onInterceptTouchEvent(MotionEvent event) { 155 if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN 156 && event.getPointerCount() == 2) { 157 mFirstPointerDownX = event.getX(0); 158 mFirstPointerDownY = event.getY(0); 159 mSecondPointerDownX = event.getX(1); 160 mSecondPointerDownY = event.getY(1); 161 mHandler.sendEmptyMessageDelayed(MESSAGE_SPEAK_WARNING, 162 SPEAK_WARNING_DELAY_MILLIS); 163 mHandler.sendEmptyMessageDelayed(MESSAGE_ENABLE_ACCESSIBILITY, 164 ENABLE_ACCESSIBILITY_DELAY_MILLIS); 165 return true; 166 } 167 return false; 168 } 169 170 public boolean onTouchEvent(MotionEvent event) { 171 final int pointerCount = event.getPointerCount(); 172 final int action = event.getActionMasked(); 173 if (mCanceled) { 174 if (action == MotionEvent.ACTION_UP) { 175 mCanceled = false; 176 } 177 return true; 178 } 179 switch (action) { 180 case MotionEvent.ACTION_POINTER_DOWN: { 181 if (pointerCount > 2) { 182 cancel(); 183 } 184 } break; 185 case MotionEvent.ACTION_MOVE: { 186 final float firstPointerMove = MathUtils.dist(event.getX(0), 187 event.getY(0), mFirstPointerDownX, mFirstPointerDownY); 188 if (Math.abs(firstPointerMove) > mTouchSlop) { 189 cancel(); 190 } 191 final float secondPointerMove = MathUtils.dist(event.getX(1), 192 event.getY(1), mSecondPointerDownX, mSecondPointerDownY); 193 if (Math.abs(secondPointerMove) > mTouchSlop) { 194 cancel(); 195 } 196 } break; 197 case MotionEvent.ACTION_POINTER_UP: 198 case MotionEvent.ACTION_CANCEL: { 199 cancel(); 200 } break; 201 } 202 return true; 203 } 204 205 private void cancel() { 206 mCanceled = true; 207 if (mHandler.hasMessages(MESSAGE_SPEAK_WARNING)) { 208 mHandler.removeMessages(MESSAGE_SPEAK_WARNING); 209 } else if (mHandler.hasMessages(MESSAGE_ENABLE_ACCESSIBILITY)) { 210 mHandler.sendEmptyMessage(MESSAGE_SPEAK_ENABLE_CANCELED); 211 } 212 mHandler.removeMessages(MESSAGE_ENABLE_ACCESSIBILITY); 213 } 214 215 private void enableAccessibility() { 216 List<AccessibilityServiceInfo> services = getInstalledSpeakingAccessibilityServices( 217 mContext); 218 if (services.isEmpty()) { 219 return; 220 } 221 boolean keyguardLocked = false; 222 try { 223 keyguardLocked = mWindowManager.isKeyguardLocked(); 224 } catch (RemoteException re) { 225 /* ignore */ 226 } 227 228 final boolean hasMoreThanOneUser = mUserManager.getUsers().size() > 1; 229 230 AccessibilityServiceInfo service = services.get(0); 231 boolean enableTouchExploration = (service.flags 232 & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0; 233 // Try to find a service supporting explore by touch. 234 if (!enableTouchExploration) { 235 final int serviceCount = services.size(); 236 for (int i = 1; i < serviceCount; i++) { 237 AccessibilityServiceInfo candidate = services.get(i); 238 if ((candidate.flags & AccessibilityServiceInfo 239 .FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0) { 240 enableTouchExploration = true; 241 service = candidate; 242 break; 243 } 244 } 245 } 246 247 ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; 248 ComponentName componentName = new ComponentName(serviceInfo.packageName, serviceInfo.name); 249 if (!keyguardLocked || !hasMoreThanOneUser) { 250 final int userId = ActivityManager.getCurrentUser(); 251 String enabledServiceString = componentName.flattenToString(); 252 ContentResolver resolver = mContext.getContentResolver(); 253 // Enable one speaking accessibility service. 254 Settings.Secure.putStringForUser(resolver, 255 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 256 enabledServiceString, userId); 257 // Allow the services we just enabled to toggle touch exploration. 258 Settings.Secure.putStringForUser(resolver, 259 Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, 260 enabledServiceString, userId); 261 // Enable touch exploration. 262 if (enableTouchExploration) { 263 Settings.Secure.putIntForUser(resolver, Settings.Secure.TOUCH_EXPLORATION_ENABLED, 264 1, userId); 265 } 266 // Enable accessibility script injection (AndroidVox) for web content. 267 Settings.Secure.putIntForUser(resolver, Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, 268 1, userId); 269 // Turn on accessibility mode last. 270 Settings.Secure.putIntForUser(resolver, Settings.Secure.ACCESSIBILITY_ENABLED, 271 1, userId); 272 } else if (keyguardLocked) { 273 try { 274 mAccessibilityManager.temporaryEnableAccessibilityStateUntilKeyguardRemoved( 275 componentName, enableTouchExploration); 276 } catch (RemoteException re) { 277 /* ignore */ 278 } 279 } 280 281 mOnAccessibilityEnabledCallback.run(); 282 } 283 } 284