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