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