• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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