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