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