1 /*
2  * Copyright (C) 2021 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.app.AlarmManager.RTC_WAKEUP;
20 
21 import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_A11Y_VIEW_AND_CONTROL_ACCESS;
22 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
23 
24 import android.Manifest;
25 import android.accessibilityservice.AccessibilityServiceInfo;
26 import android.annotation.MainThread;
27 import android.annotation.NonNull;
28 import android.app.ActivityOptions;
29 import android.app.AlarmManager;
30 import android.app.Notification;
31 import android.app.NotificationManager;
32 import android.app.PendingIntent;
33 import android.app.StatusBarManager;
34 import android.content.BroadcastReceiver;
35 import android.content.ComponentName;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.IntentFilter;
39 import android.graphics.Bitmap;
40 import android.graphics.drawable.Drawable;
41 import android.os.Bundle;
42 import android.os.Handler;
43 import android.os.SystemClock;
44 import android.os.UserHandle;
45 import android.provider.Settings;
46 import android.text.TextUtils;
47 import android.util.ArraySet;
48 import android.view.accessibility.AccessibilityManager;
49 
50 import com.android.internal.R;
51 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
52 import com.android.internal.annotations.VisibleForTesting;
53 import com.android.internal.notification.SystemNotificationChannels;
54 import com.android.internal.util.ImageUtils;
55 
56 import java.util.ArrayList;
57 import java.util.Calendar;
58 import java.util.Iterator;
59 import java.util.List;
60 import java.util.Set;
61 
62 /**
63  * The class handles permission warning notifications for not accessibility-categorized
64  * accessibility services from {@link AccessibilitySecurityPolicy}. And also maintains the setting
65  * {@link Settings.Secure#NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES} in order not to
66  * resend notifications to the same service.
67  */
68 public class PolicyWarningUIController {
69     private static final String TAG = PolicyWarningUIController.class.getSimpleName();
70     @VisibleForTesting
71     protected static final String ACTION_SEND_NOTIFICATION = TAG + ".ACTION_SEND_NOTIFICATION";
72     @VisibleForTesting
73     protected static final String ACTION_A11Y_SETTINGS = TAG + ".ACTION_A11Y_SETTINGS";
74     @VisibleForTesting
75     protected static final String ACTION_DISMISS_NOTIFICATION =
76             TAG + ".ACTION_DISMISS_NOTIFICATION";
77     private static final String EXTRA_TIME_FOR_LOGGING = "start_time_to_log_a11y_tool";
78     private static final int SEND_NOTIFICATION_DELAY_HOURS = 24;
79 
80     /** Current enabled accessibility services. */
81     private final ArraySet<ComponentName> mEnabledA11yServices = new ArraySet<>();
82 
83     private final Handler mMainHandler;
84     private final AlarmManager mAlarmManager;
85     private final Context mContext;
86     private final NotificationController mNotificationController;
87 
PolicyWarningUIController(@onNull Handler handler, @NonNull Context context, NotificationController notificationController)88     public PolicyWarningUIController(@NonNull Handler handler, @NonNull Context context,
89             NotificationController notificationController) {
90         mMainHandler = handler;
91         mContext = context;
92         mNotificationController = notificationController;
93         mAlarmManager = mContext.getSystemService(AlarmManager.class);
94         final IntentFilter filter = new IntentFilter();
95         filter.addAction(ACTION_SEND_NOTIFICATION);
96         filter.addAction(ACTION_A11Y_SETTINGS);
97         filter.addAction(ACTION_DISMISS_NOTIFICATION);
98         mContext.registerReceiver(mNotificationController, filter,
99                 Manifest.permission.MANAGE_ACCESSIBILITY, mMainHandler, Context.RECEIVER_EXPORTED);
100     }
101 
102     /**
103      * Updates enabled accessibility services and notified accessibility services after switching
104      * to another user.
105      *
106      * @param enabledServices the current enabled services
107      */
onSwitchUser(int userId, Set<ComponentName> enabledServices)108     public void onSwitchUser(int userId, Set<ComponentName> enabledServices) {
109         mMainHandler.sendMessage(
110                 obtainMessage(this::onSwitchUserInternal, userId, enabledServices));
111     }
112 
onSwitchUserInternal(int userId, Set<ComponentName> enabledServices)113     private void onSwitchUserInternal(int userId, Set<ComponentName> enabledServices) {
114         mEnabledA11yServices.clear();
115         mEnabledA11yServices.addAll(enabledServices);
116         mNotificationController.onSwitchUser(userId);
117     }
118 
119     /**
120      * Computes the newly disabled services and removes its record from the setting
121      * {@link Settings.Secure#NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES} after detecting the
122      * setting {@link Settings.Secure#ENABLED_ACCESSIBILITY_SERVICES} changed.
123      *
124      * @param userId          The user id
125      * @param enabledServices The enabled services set
126      */
onEnabledServicesChanged(int userId, Set<ComponentName> enabledServices)127     public void onEnabledServicesChanged(int userId, Set<ComponentName> enabledServices) {
128         mMainHandler.sendMessage(
129                 obtainMessage(this::onEnabledServicesChangedInternal, userId, enabledServices));
130     }
131 
onEnabledServicesChangedInternal(int userId, Set<ComponentName> enabledServices)132     void onEnabledServicesChangedInternal(int userId, Set<ComponentName> enabledServices) {
133         final ArraySet<ComponentName> disabledServices = new ArraySet<>(mEnabledA11yServices);
134         disabledServices.removeAll(enabledServices);
135         mEnabledA11yServices.clear();
136         mEnabledA11yServices.addAll(enabledServices);
137         mMainHandler.sendMessage(
138                 obtainMessage(mNotificationController::onServicesDisabled, userId,
139                         disabledServices));
140     }
141 
142     /**
143      * Called when the target service is bound. Sets an 24 hours alarm to the service which is not
144      * notified yet to execute action {@code ACTION_SEND_NOTIFICATION}.
145      *
146      * @param userId  The user id
147      * @param service The service's component name
148      */
onNonA11yCategoryServiceBound(int userId, ComponentName service)149     public void onNonA11yCategoryServiceBound(int userId, ComponentName service) {
150         mMainHandler.sendMessage(obtainMessage(this::setAlarm, userId, service));
151     }
152 
153     /**
154      * Called when the target service is unbound. Cancels the old alarm with intent action
155      * {@code ACTION_SEND_NOTIFICATION} from the service.
156      *
157      * @param userId  The user id
158      * @param service The service's component name
159      */
onNonA11yCategoryServiceUnbound(int userId, ComponentName service)160     public void onNonA11yCategoryServiceUnbound(int userId, ComponentName service) {
161         mMainHandler.sendMessage(obtainMessage(this::cancelAlarm, userId, service));
162     }
163 
setAlarm(int userId, ComponentName service)164     private void setAlarm(int userId, ComponentName service) {
165         final Calendar cal = Calendar.getInstance();
166         cal.add(Calendar.HOUR, SEND_NOTIFICATION_DELAY_HOURS);
167         mAlarmManager.set(RTC_WAKEUP, cal.getTimeInMillis(),
168                 createPendingIntent(mContext, userId, ACTION_SEND_NOTIFICATION, service));
169     }
170 
cancelAlarm(int userId, ComponentName service)171     private void cancelAlarm(int userId, ComponentName service) {
172         mAlarmManager.cancel(
173                 createPendingIntent(mContext, userId, ACTION_SEND_NOTIFICATION, service));
174     }
175 
createPendingIntent(Context context, int userId, String action, ComponentName serviceComponentName)176     protected static PendingIntent createPendingIntent(Context context, int userId, String action,
177             ComponentName serviceComponentName) {
178         return PendingIntent.getBroadcast(context, 0,
179                 createIntent(context, userId, action, serviceComponentName),
180                 PendingIntent.FLAG_IMMUTABLE);
181     }
182 
createIntent(Context context, int userId, String action, ComponentName serviceComponentName)183     protected static Intent createIntent(Context context, int userId, String action,
184             ComponentName serviceComponentName) {
185         final Intent intent = new Intent(action);
186         intent.setPackage(context.getPackageName())
187                 .setIdentifier(serviceComponentName.flattenToShortString())
188                 .putExtra(Intent.EXTRA_COMPONENT_NAME, serviceComponentName)
189                 .putExtra(Intent.EXTRA_USER_ID, userId)
190                 .putExtra(Intent.EXTRA_TIME, SystemClock.elapsedRealtime());
191         return intent;
192     }
193 
194     /**
195      * Enables to send the notification for non-Accessibility services.
196      */
enableSendingNonA11yToolNotification(boolean enable)197     public void enableSendingNonA11yToolNotification(boolean enable) {
198         mMainHandler.sendMessage(
199                 obtainMessage(this::enableSendingNonA11yToolNotificationInternal, enable));
200     }
201 
enableSendingNonA11yToolNotificationInternal(boolean enable)202     private void enableSendingNonA11yToolNotificationInternal(boolean enable) {
203         mNotificationController.setSendingNotification(enable);
204     }
205 
206     /** A sub class to handle notifications and settings on the main thread. */
207     @MainThread
208     public static class NotificationController extends BroadcastReceiver {
209         private static final char RECORD_SEPARATOR = ':';
210 
211         /** All accessibility services which are notified to the user by the policy warning rule. */
212         private final ArraySet<ComponentName> mNotifiedA11yServices = new ArraySet<>();
213         /** The component name of sent notifications. */
214         private final List<ComponentName> mSentA11yServiceNotification = new ArrayList<>();
215         private final NotificationManager mNotificationManager;
216         private final Context mContext;
217 
218         private int mCurrentUserId;
219         private boolean mSendNotification;
220 
NotificationController(Context context)221         public NotificationController(Context context) {
222             mContext = context;
223             mNotificationManager = mContext.getSystemService(NotificationManager.class);
224         }
225 
226         @Override
onReceive(Context context, Intent intent)227         public void onReceive(Context context, Intent intent) {
228             final String action = intent.getAction();
229             final ComponentName componentName = intent.getParcelableExtra(
230                     Intent.EXTRA_COMPONENT_NAME, android.content.ComponentName.class);
231             if (TextUtils.isEmpty(action) || componentName == null) {
232                 return;
233             }
234             final long startTimeMills = intent.getLongExtra(Intent.EXTRA_TIME, 0);
235             final long durationMills =
236                     startTimeMills > 0 ? SystemClock.elapsedRealtime() - startTimeMills : 0;
237             final int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_SYSTEM);
238             if (ACTION_SEND_NOTIFICATION.equals(action)) {
239                 if (trySendNotification(userId, componentName)) {
240                     AccessibilityStatsLogUtils.logNonA11yToolServiceWarningReported(
241                             componentName.getPackageName(),
242                             AccessibilityStatsLogUtils.ACCESSIBILITY_PRIVACY_WARNING_STATUS_SHOWN,
243                             durationMills);
244                 }
245             } else if (ACTION_A11Y_SETTINGS.equals(action)) {
246                 if (tryLaunchSettings(userId, componentName)) {
247                     AccessibilityStatsLogUtils.logNonA11yToolServiceWarningReported(
248                             componentName.getPackageName(),
249                             AccessibilityStatsLogUtils.ACCESSIBILITY_PRIVACY_WARNING_STATUS_CLICKED,
250                             durationMills);
251                 }
252                 mNotificationManager.cancel(componentName.flattenToShortString(),
253                         NOTE_A11Y_VIEW_AND_CONTROL_ACCESS);
254                 mSentA11yServiceNotification.remove(componentName);
255                 onNotificationCanceled(userId, componentName);
256             } else if (ACTION_DISMISS_NOTIFICATION.equals(action)) {
257                 mSentA11yServiceNotification.remove(componentName);
258                 onNotificationCanceled(userId, componentName);
259             }
260         }
261 
onSwitchUser(int userId)262         protected void onSwitchUser(int userId) {
263             cancelSentNotifications();
264             mNotifiedA11yServices.clear();
265             mCurrentUserId = userId;
266             mNotifiedA11yServices.addAll(readNotifiedServiceList(userId));
267         }
268 
onServicesDisabled(int userId, ArraySet<ComponentName> disabledServices)269         protected void onServicesDisabled(int userId,
270                 ArraySet<ComponentName> disabledServices) {
271             if (mNotifiedA11yServices.removeAll(disabledServices)) {
272                 writeNotifiedServiceList(userId, mNotifiedA11yServices);
273             }
274         }
275 
trySendNotification(int userId, ComponentName componentName)276         private boolean trySendNotification(int userId, ComponentName componentName) {
277             if (userId != mCurrentUserId) {
278                 return false;
279             }
280 
281             if (!mSendNotification) {
282                 return false;
283             }
284 
285             List<AccessibilityServiceInfo> enabledServiceInfos = getEnabledServiceInfos();
286             for (int i = 0; i < enabledServiceInfos.size(); i++) {
287                 final AccessibilityServiceInfo a11yServiceInfo = enabledServiceInfos.get(i);
288                 if (componentName.flattenToShortString().equals(
289                         a11yServiceInfo.getComponentName().flattenToShortString())) {
290                     if (!a11yServiceInfo.isAccessibilityTool()
291                             && !mNotifiedA11yServices.contains(componentName)) {
292                         final CharSequence displayName =
293                                 a11yServiceInfo.getResolveInfo().serviceInfo.loadLabel(
294                                         mContext.getPackageManager());
295                         final Drawable drawable = a11yServiceInfo.getResolveInfo().loadIcon(
296                                 mContext.getPackageManager());
297                         final int size = mContext.getResources().getDimensionPixelSize(
298                                 android.R.dimen.app_icon_size);
299                         sendNotification(userId, componentName, displayName,
300                                 ImageUtils.buildScaledBitmap(drawable, size, size));
301                         return true;
302                     }
303                     break;
304                 }
305             }
306             return false;
307         }
308 
tryLaunchSettings(int userId, ComponentName componentName)309         private boolean tryLaunchSettings(int userId, ComponentName componentName) {
310             if (userId != mCurrentUserId) {
311                 return false;
312             }
313             final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS);
314             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
315             intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName.flattenToShortString());
316             intent.putExtra(EXTRA_TIME_FOR_LOGGING, SystemClock.elapsedRealtime());
317             final Bundle bundle = ActivityOptions.makeBasic().setLaunchDisplayId(
318                     mContext.getDisplayId()).toBundle();
319             mContext.startActivityAsUser(intent, bundle, UserHandle.of(userId));
320             mContext.getSystemService(StatusBarManager.class).collapsePanels();
321             return true;
322         }
323 
onNotificationCanceled(int userId, ComponentName componentName)324         protected void onNotificationCanceled(int userId, ComponentName componentName) {
325             if (userId != mCurrentUserId) {
326                 return;
327             }
328 
329             if (mNotifiedA11yServices.add(componentName)) {
330                 writeNotifiedServiceList(userId, mNotifiedA11yServices);
331             }
332         }
333 
sendNotification(int userId, ComponentName serviceComponentName, CharSequence name, Bitmap bitmap)334         private void sendNotification(int userId, ComponentName serviceComponentName,
335                 CharSequence name,
336                 Bitmap bitmap) {
337             final Notification.Builder notificationBuilder = new Notification.Builder(mContext,
338                     SystemNotificationChannels.ACCESSIBILITY_SECURITY_POLICY);
339             notificationBuilder.setSmallIcon(R.drawable.ic_accessibility_24dp)
340                     .setContentTitle(
341                             mContext.getString(R.string.view_and_control_notification_title))
342                     .setContentText(
343                             mContext.getString(R.string.view_and_control_notification_content,
344                                     name))
345                     .setStyle(new Notification.BigTextStyle()
346                             .bigText(
347                                     mContext.getString(
348                                             R.string.view_and_control_notification_content,
349                                             name)))
350                     .setTicker(mContext.getString(R.string.view_and_control_notification_title))
351                     .setOnlyAlertOnce(true)
352                     .setDeleteIntent(
353                             createPendingIntent(mContext, userId, ACTION_DISMISS_NOTIFICATION,
354                                     serviceComponentName))
355                     .setContentIntent(
356                             createPendingIntent(mContext, userId, ACTION_A11Y_SETTINGS,
357                                     serviceComponentName));
358             if (bitmap != null) {
359                 notificationBuilder.setLargeIcon(bitmap);
360             }
361             mNotificationManager.notify(serviceComponentName.flattenToShortString(),
362                     NOTE_A11Y_VIEW_AND_CONTROL_ACCESS,
363                     notificationBuilder.build());
364             mSentA11yServiceNotification.add(serviceComponentName);
365         }
366 
readNotifiedServiceList(int userId)367         private ArraySet<ComponentName> readNotifiedServiceList(int userId) {
368             final String notifiedServiceSetting = Settings.Secure.getStringForUser(
369                     mContext.getContentResolver(),
370                     Settings.Secure.NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES,
371                     userId);
372             if (TextUtils.isEmpty(notifiedServiceSetting)) {
373                 return new ArraySet<>();
374             }
375 
376             final TextUtils.StringSplitter componentNameSplitter =
377                     new TextUtils.SimpleStringSplitter(RECORD_SEPARATOR);
378             componentNameSplitter.setString(notifiedServiceSetting);
379 
380             final ArraySet<ComponentName> notifiedServices = new ArraySet<>();
381             final Iterator<String> it = componentNameSplitter.iterator();
382             while (it.hasNext()) {
383                 final String componentNameString = it.next();
384                 final ComponentName notifiedService = ComponentName.unflattenFromString(
385                         componentNameString);
386                 if (notifiedService != null) {
387                     notifiedServices.add(notifiedService);
388                 }
389             }
390             return notifiedServices;
391         }
392 
writeNotifiedServiceList(int userId, ArraySet<ComponentName> services)393         private void writeNotifiedServiceList(int userId, ArraySet<ComponentName> services) {
394             StringBuilder notifiedServicesBuilder = new StringBuilder();
395             for (int i = 0; i < services.size(); i++) {
396                 if (i > 0) {
397                     notifiedServicesBuilder.append(RECORD_SEPARATOR);
398                 }
399                 final ComponentName notifiedService = services.valueAt(i);
400                 notifiedServicesBuilder.append(notifiedService.flattenToShortString());
401             }
402             Settings.Secure.putStringForUser(mContext.getContentResolver(),
403                     Settings.Secure.NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES,
404                     notifiedServicesBuilder.toString(), userId);
405         }
406 
407         @VisibleForTesting
getEnabledServiceInfos()408         protected List<AccessibilityServiceInfo> getEnabledServiceInfos() {
409             final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(
410                     mContext);
411             return accessibilityManager.getEnabledAccessibilityServiceList(
412                     AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
413         }
414 
cancelSentNotifications()415         private void cancelSentNotifications() {
416             mSentA11yServiceNotification.forEach(componentName -> mNotificationManager.cancel(
417                     componentName.flattenToShortString(), NOTE_A11Y_VIEW_AND_CONTROL_ACCESS));
418             mSentA11yServiceNotification.clear();
419         }
420 
setSendingNotification(boolean enable)421         void setSendingNotification(boolean enable) {
422             mSendNotification = enable;
423         }
424     }
425 }
426