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