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