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