1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.accessibility; 18 19 import android.accessibilityservice.AccessibilityService; 20 import android.app.PendingIntent; 21 import android.app.RemoteAction; 22 import android.app.StatusBarManager; 23 import android.content.Context; 24 import android.hardware.input.InputManager; 25 import android.os.Binder; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.os.PowerManager; 29 import android.os.SystemClock; 30 import android.util.ArrayMap; 31 import android.util.Slog; 32 import android.view.InputDevice; 33 import android.view.KeyCharacterMap; 34 import android.view.KeyEvent; 35 import android.view.WindowManager; 36 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 37 38 import com.android.internal.R; 39 import com.android.internal.accessibility.util.AccessibilityUtils; 40 import com.android.internal.annotations.GuardedBy; 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.internal.util.ScreenshotHelper; 43 import com.android.server.LocalServices; 44 import com.android.server.statusbar.StatusBarManagerInternal; 45 import com.android.server.wm.WindowManagerInternal; 46 47 import java.util.ArrayList; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.function.Supplier; 51 52 /** 53 * Handle the back-end of system AccessibilityAction. 54 * 55 * This class should support three use cases with combined usage of new API and legacy API: 56 * 57 * Use case 1: SystemUI doesn't use the new system action registration API. Accessibility 58 * service doesn't use the new system action API to obtain action list. Accessibility 59 * service uses legacy global action id to perform predefined system actions. 60 * Use case 2: SystemUI uses the new system action registration API to register available system 61 * actions. Accessibility service doesn't use the new system action API to obtain action 62 * list. Accessibility service uses legacy global action id to trigger the system 63 * actions registered by SystemUI. 64 * Use case 3: SystemUI doesn't use the new system action registration API.Accessibility service 65 * obtains the available system actions using new AccessibilityService API and trigger 66 * the predefined system actions. 67 */ 68 public class SystemActionPerformer { 69 private static final String TAG = "SystemActionPerformer"; 70 71 interface SystemActionsChangedListener { onSystemActionsChanged()72 void onSystemActionsChanged(); 73 } 74 private final SystemActionsChangedListener mListener; 75 76 interface DisplayUpdateCallBack { moveNonProxyTopFocusedDisplayToTopIfNeeded()77 void moveNonProxyTopFocusedDisplayToTopIfNeeded(); 78 getLastNonProxyTopFocusedDisplayId()79 int getLastNonProxyTopFocusedDisplayId(); 80 } 81 private final DisplayUpdateCallBack mDisplayUpdateCallBack; 82 83 private final Object mSystemActionLock = new Object(); 84 // Resource id based ActionId -> RemoteAction 85 @GuardedBy("mSystemActionLock") 86 private final Map<Integer, RemoteAction> mRegisteredSystemActions = new ArrayMap<>(); 87 88 // Legacy system actions. 89 private final AccessibilityAction mLegacyHomeAction; 90 private final AccessibilityAction mLegacyBackAction; 91 private final AccessibilityAction mLegacyRecentsAction; 92 private final AccessibilityAction mLegacyNotificationsAction; 93 private final AccessibilityAction mLegacyQuickSettingsAction; 94 private final AccessibilityAction mLegacyPowerDialogAction; 95 private final AccessibilityAction mLegacyLockScreenAction; 96 private final AccessibilityAction mLegacyTakeScreenshotAction; 97 98 private final WindowManagerInternal mWindowManagerService; 99 private final Context mContext; 100 private Supplier<ScreenshotHelper> mScreenshotHelperSupplier; 101 SystemActionPerformer( Context context, WindowManagerInternal windowManagerInternal)102 public SystemActionPerformer( 103 Context context, 104 WindowManagerInternal windowManagerInternal) { 105 this(context, windowManagerInternal, null, null, null); 106 } 107 108 // Used to mock ScreenshotHelper 109 @VisibleForTesting SystemActionPerformer( Context context, WindowManagerInternal windowManagerInternal, Supplier<ScreenshotHelper> screenshotHelperSupplier)110 public SystemActionPerformer( 111 Context context, 112 WindowManagerInternal windowManagerInternal, 113 Supplier<ScreenshotHelper> screenshotHelperSupplier) { 114 this(context, windowManagerInternal, screenshotHelperSupplier, null, null); 115 } 116 SystemActionPerformer( Context context, WindowManagerInternal windowManagerInternal, Supplier<ScreenshotHelper> screenshotHelperSupplier, SystemActionsChangedListener listener, DisplayUpdateCallBack callback)117 public SystemActionPerformer( 118 Context context, 119 WindowManagerInternal windowManagerInternal, 120 Supplier<ScreenshotHelper> screenshotHelperSupplier, 121 SystemActionsChangedListener listener, 122 DisplayUpdateCallBack callback) { 123 mContext = context; 124 mWindowManagerService = windowManagerInternal; 125 mListener = listener; 126 mDisplayUpdateCallBack = callback; 127 mScreenshotHelperSupplier = screenshotHelperSupplier; 128 129 mLegacyHomeAction = new AccessibilityAction( 130 AccessibilityService.GLOBAL_ACTION_HOME, 131 mContext.getResources().getString( 132 R.string.accessibility_system_action_home_label)); 133 mLegacyBackAction = new AccessibilityAction( 134 AccessibilityService.GLOBAL_ACTION_BACK, 135 mContext.getResources().getString( 136 R.string.accessibility_system_action_back_label)); 137 mLegacyRecentsAction = new AccessibilityAction( 138 AccessibilityService.GLOBAL_ACTION_RECENTS, 139 mContext.getResources().getString( 140 R.string.accessibility_system_action_recents_label)); 141 mLegacyNotificationsAction = new AccessibilityAction( 142 AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS, 143 mContext.getResources().getString( 144 R.string.accessibility_system_action_notifications_label)); 145 mLegacyQuickSettingsAction = new AccessibilityAction( 146 AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS, 147 mContext.getResources().getString( 148 R.string.accessibility_system_action_quick_settings_label)); 149 mLegacyPowerDialogAction = new AccessibilityAction( 150 AccessibilityService.GLOBAL_ACTION_POWER_DIALOG, 151 mContext.getResources().getString( 152 R.string.accessibility_system_action_power_dialog_label)); 153 mLegacyLockScreenAction = new AccessibilityAction( 154 AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN, 155 mContext.getResources().getString( 156 R.string.accessibility_system_action_lock_screen_label)); 157 mLegacyTakeScreenshotAction = new AccessibilityAction( 158 AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT, 159 mContext.getResources().getString( 160 R.string.accessibility_system_action_screenshot_label)); 161 } 162 163 /** 164 * This method is called to register a system action. If a system action is already registered 165 * with the given id, the existing system action will be overwritten. 166 * 167 * This method is supposed to be package internal since this class is meant to be used by 168 * AccessibilityManagerService only. But Mockito has a bug which requiring this to be public 169 * to be mocked. 170 */ 171 @VisibleForTesting registerSystemAction(int id, RemoteAction action)172 public void registerSystemAction(int id, RemoteAction action) { 173 synchronized (mSystemActionLock) { 174 mRegisteredSystemActions.put(id, action); 175 } 176 if (mListener != null) { 177 mListener.onSystemActionsChanged(); 178 } 179 } 180 181 /** 182 * This method is called to unregister a system action previously registered through 183 * registerSystemAction. 184 * 185 * This method is supposed to be package internal since this class is meant to be used by 186 * AccessibilityManagerService only. But Mockito has a bug which requiring this to be public 187 * to be mocked. 188 */ 189 @VisibleForTesting unregisterSystemAction(int id)190 public void unregisterSystemAction(int id) { 191 synchronized (mSystemActionLock) { 192 mRegisteredSystemActions.remove(id); 193 } 194 if (mListener != null) { 195 mListener.onSystemActionsChanged(); 196 } 197 } 198 199 /** 200 * This method returns the list of available system actions. 201 */ 202 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) getSystemActions()203 public List<AccessibilityAction> getSystemActions() { 204 List<AccessibilityAction> systemActions = new ArrayList<>(); 205 synchronized (mSystemActionLock) { 206 for (Map.Entry<Integer, RemoteAction> entry : mRegisteredSystemActions.entrySet()) { 207 AccessibilityAction systemAction = new AccessibilityAction( 208 entry.getKey(), 209 entry.getValue().getTitle()); 210 systemActions.add(systemAction); 211 } 212 213 // add AccessibilitySystemAction entry for legacy system actions if not overwritten 214 addLegacySystemActions(systemActions); 215 } 216 return systemActions; 217 } 218 addLegacySystemActions(List<AccessibilityAction> systemActions)219 private void addLegacySystemActions(List<AccessibilityAction> systemActions) { 220 if (!mRegisteredSystemActions.containsKey(AccessibilityService.GLOBAL_ACTION_BACK)) { 221 systemActions.add(mLegacyBackAction); 222 } 223 if (!mRegisteredSystemActions.containsKey(AccessibilityService.GLOBAL_ACTION_HOME)) { 224 systemActions.add(mLegacyHomeAction); 225 } 226 if (!mRegisteredSystemActions.containsKey(AccessibilityService.GLOBAL_ACTION_RECENTS)) { 227 systemActions.add(mLegacyRecentsAction); 228 } 229 if (!mRegisteredSystemActions.containsKey( 230 AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS)) { 231 systemActions.add(mLegacyNotificationsAction); 232 } 233 if (!mRegisteredSystemActions.containsKey( 234 AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS)) { 235 systemActions.add(mLegacyQuickSettingsAction); 236 } 237 if (!mRegisteredSystemActions.containsKey( 238 AccessibilityService.GLOBAL_ACTION_POWER_DIALOG)) { 239 systemActions.add(mLegacyPowerDialogAction); 240 } 241 if (!mRegisteredSystemActions.containsKey( 242 AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN)) { 243 systemActions.add(mLegacyLockScreenAction); 244 } 245 if (!mRegisteredSystemActions.containsKey( 246 AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT)) { 247 systemActions.add(mLegacyTakeScreenshotAction); 248 } 249 } 250 251 /** 252 * Trigger the registered action by the matching action id. 253 */ performSystemAction(int actionId)254 public boolean performSystemAction(int actionId) { 255 final long identity = Binder.clearCallingIdentity(); 256 try { 257 synchronized (mSystemActionLock) { 258 mDisplayUpdateCallBack.moveNonProxyTopFocusedDisplayToTopIfNeeded(); 259 // If a system action is registered with the given actionId, call the corresponding 260 // RemoteAction. 261 RemoteAction registeredAction = mRegisteredSystemActions.get(actionId); 262 if (registeredAction != null) { 263 try { 264 registeredAction.getActionIntent().send(); 265 return true; 266 } catch (PendingIntent.CanceledException ex) { 267 Slog.e(TAG, 268 "canceled PendingIntent for global action " 269 + registeredAction.getTitle(), 270 ex); 271 } 272 return false; 273 } 274 } 275 276 // No RemoteAction registered with the given actionId, try the default legacy system 277 // actions. 278 switch (actionId) { 279 case AccessibilityService.GLOBAL_ACTION_BACK: { 280 sendDownAndUpKeyEvents(KeyEvent.KEYCODE_BACK, InputDevice.SOURCE_KEYBOARD); 281 return true; 282 } 283 case AccessibilityService.GLOBAL_ACTION_HOME: { 284 sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HOME, InputDevice.SOURCE_KEYBOARD); 285 return true; 286 } 287 case AccessibilityService.GLOBAL_ACTION_RECENTS: 288 return openRecents(); 289 case AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS: { 290 expandNotifications(); 291 return true; 292 } 293 case AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS: { 294 expandQuickSettings(); 295 return true; 296 } 297 case AccessibilityService.GLOBAL_ACTION_POWER_DIALOG: { 298 showGlobalActions(); 299 return true; 300 } 301 case AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN: 302 return lockScreen(); 303 case AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT: 304 return takeScreenshot(); 305 case AccessibilityService.GLOBAL_ACTION_KEYCODE_HEADSETHOOK: 306 if (!AccessibilityUtils.interceptHeadsetHookForActiveCall(mContext)) { 307 sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HEADSETHOOK, 308 InputDevice.SOURCE_KEYBOARD); 309 } 310 return true; 311 case AccessibilityService.GLOBAL_ACTION_DPAD_UP: 312 sendDownAndUpKeyEvents(KeyEvent.KEYCODE_DPAD_UP, 313 InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD); 314 return true; 315 case AccessibilityService.GLOBAL_ACTION_DPAD_DOWN: 316 sendDownAndUpKeyEvents(KeyEvent.KEYCODE_DPAD_DOWN, 317 InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD); 318 return true; 319 case AccessibilityService.GLOBAL_ACTION_DPAD_LEFT: 320 sendDownAndUpKeyEvents(KeyEvent.KEYCODE_DPAD_LEFT, 321 InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD); 322 return true; 323 case AccessibilityService.GLOBAL_ACTION_DPAD_RIGHT: 324 sendDownAndUpKeyEvents(KeyEvent.KEYCODE_DPAD_RIGHT, 325 InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD); 326 return true; 327 case AccessibilityService.GLOBAL_ACTION_DPAD_CENTER: 328 sendDownAndUpKeyEvents(KeyEvent.KEYCODE_DPAD_CENTER, 329 InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD); 330 return true; 331 default: 332 Slog.e(TAG, "Invalid action id: " + actionId); 333 return false; 334 } 335 } finally { 336 Binder.restoreCallingIdentity(identity); 337 } 338 } 339 sendDownAndUpKeyEvents(int keyCode, int source)340 private void sendDownAndUpKeyEvents(int keyCode, int source) { 341 final long token = Binder.clearCallingIdentity(); 342 try { 343 // Inject down. 344 final long downTime = SystemClock.uptimeMillis(); 345 sendKeyEventIdentityCleared(keyCode, KeyEvent.ACTION_DOWN, downTime, downTime, source); 346 sendKeyEventIdentityCleared( 347 keyCode, KeyEvent.ACTION_UP, downTime, SystemClock.uptimeMillis(), source); 348 } finally { 349 Binder.restoreCallingIdentity(token); 350 } 351 } 352 sendKeyEventIdentityCleared(int keyCode, int action, long downTime, long time, int source)353 private void sendKeyEventIdentityCleared(int keyCode, int action, long downTime, long time, 354 int source) { 355 KeyEvent event = KeyEvent.obtain(downTime, time, action, keyCode, 0, 0, 356 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FROM_SYSTEM, 357 source, mDisplayUpdateCallBack.getLastNonProxyTopFocusedDisplayId(), null); 358 mContext.getSystemService(InputManager.class) 359 .injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); 360 event.recycle(); 361 } 362 expandNotifications()363 private void expandNotifications() { 364 final long token = Binder.clearCallingIdentity(); 365 try { 366 StatusBarManager statusBarManager = (StatusBarManager) mContext.getSystemService( 367 android.app.Service.STATUS_BAR_SERVICE); 368 statusBarManager.expandNotificationsPanel(); 369 } finally { 370 Binder.restoreCallingIdentity(token); 371 } 372 } 373 expandQuickSettings()374 private void expandQuickSettings() { 375 final long token = Binder.clearCallingIdentity(); 376 try { 377 StatusBarManager statusBarManager = (StatusBarManager) mContext.getSystemService( 378 android.app.Service.STATUS_BAR_SERVICE); 379 statusBarManager.expandSettingsPanel(); 380 } finally { 381 Binder.restoreCallingIdentity(token); 382 } 383 } 384 openRecents()385 private boolean openRecents() { 386 final long token = Binder.clearCallingIdentity(); 387 try { 388 StatusBarManagerInternal statusBarService = LocalServices.getService( 389 StatusBarManagerInternal.class); 390 if (statusBarService == null) { 391 return false; 392 } 393 statusBarService.toggleRecentApps(); 394 } finally { 395 Binder.restoreCallingIdentity(token); 396 } 397 return true; 398 } 399 showGlobalActions()400 private void showGlobalActions() { 401 mWindowManagerService.showGlobalActions(); 402 } 403 lockScreen()404 private boolean lockScreen() { 405 mContext.getSystemService(PowerManager.class).goToSleep(SystemClock.uptimeMillis(), 406 PowerManager.GO_TO_SLEEP_REASON_ACCESSIBILITY, 0); 407 mWindowManagerService.lockNow(); 408 return true; 409 } 410 takeScreenshot()411 private boolean takeScreenshot() { 412 ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null) 413 ? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext); 414 screenshotHelper.takeScreenshot( 415 WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS, 416 new Handler(Looper.getMainLooper()), null); 417 return true; 418 } 419 } 420