blob: 7fd859ca9333db5ee510132a9d7f0f77aa958bc3 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.accessibility;
import static android.app.AlarmManager.RTC_WAKEUP;
import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_A11Y_VIEW_AND_CONTROL_ACCESS;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.Manifest;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.app.ActivityOptions;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.StatusBarManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArraySet;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.R;
import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.ImageUtils;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* The class handles permission warning notifications for not accessibility-categorized
* accessibility services from {@link AccessibilitySecurityPolicy}. And also maintains the setting
* {@link Settings.Secure#NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES} in order not to
* resend notifications to the same service.
*/
public class PolicyWarningUIController {
private static final String TAG = PolicyWarningUIController.class.getSimpleName();
@VisibleForTesting
protected static final String ACTION_SEND_NOTIFICATION = TAG + ".ACTION_SEND_NOTIFICATION";
@VisibleForTesting
protected static final String ACTION_A11Y_SETTINGS = TAG + ".ACTION_A11Y_SETTINGS";
@VisibleForTesting
protected static final String ACTION_DISMISS_NOTIFICATION =
TAG + ".ACTION_DISMISS_NOTIFICATION";
private static final String EXTRA_TIME_FOR_LOGGING = "start_time_to_log_a11y_tool";
private static final int SEND_NOTIFICATION_DELAY_HOURS = 24;
/** Current enabled accessibility services. */
private final ArraySet<ComponentName> mEnabledA11yServices = new ArraySet<>();
private final Handler mMainHandler;
private final AlarmManager mAlarmManager;
private final Context mContext;
private final NotificationController mNotificationController;
public PolicyWarningUIController(@NonNull Handler handler, @NonNull Context context,
NotificationController notificationController) {
mMainHandler = handler;
mContext = context;
mNotificationController = notificationController;
mAlarmManager = mContext.getSystemService(AlarmManager.class);
final IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_SEND_NOTIFICATION);
filter.addAction(ACTION_A11Y_SETTINGS);
filter.addAction(ACTION_DISMISS_NOTIFICATION);
mContext.registerReceiver(mNotificationController, filter,
Manifest.permission.MANAGE_ACCESSIBILITY, mMainHandler, Context.RECEIVER_EXPORTED);
}
/**
* Updates enabled accessibility services and notified accessibility services after switching
* to another user.
*
* @param enabledServices the current enabled services
*/
public void onSwitchUser(int userId, Set<ComponentName> enabledServices) {
mMainHandler.sendMessage(
obtainMessage(this::onSwitchUserInternal, userId, enabledServices));
}
private void onSwitchUserInternal(int userId, Set<ComponentName> enabledServices) {
mEnabledA11yServices.clear();
mEnabledA11yServices.addAll(enabledServices);
mNotificationController.onSwitchUser(userId);
}
/**
* Computes the newly disabled services and removes its record from the setting
* {@link Settings.Secure#NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES} after detecting the
* setting {@link Settings.Secure#ENABLED_ACCESSIBILITY_SERVICES} changed.
*
* @param userId The user id
* @param enabledServices The enabled services set
*/
public void onEnabledServicesChanged(int userId, Set<ComponentName> enabledServices) {
mMainHandler.sendMessage(
obtainMessage(this::onEnabledServicesChangedInternal, userId, enabledServices));
}
void onEnabledServicesChangedInternal(int userId, Set<ComponentName> enabledServices) {
final ArraySet<ComponentName> disabledServices = new ArraySet<>(mEnabledA11yServices);
disabledServices.removeAll(enabledServices);
mEnabledA11yServices.clear();
mEnabledA11yServices.addAll(enabledServices);
mMainHandler.sendMessage(
obtainMessage(mNotificationController::onServicesDisabled, userId,
disabledServices));
}
/**
* Called when the target service is bound. Sets an 24 hours alarm to the service which is not
* notified yet to execute action {@code ACTION_SEND_NOTIFICATION}.
*
* @param userId The user id
* @param service The service's component name
*/
public void onNonA11yCategoryServiceBound(int userId, ComponentName service) {
mMainHandler.sendMessage(obtainMessage(this::setAlarm, userId, service));
}
/**
* Called when the target service is unbound. Cancels the old alarm with intent action
* {@code ACTION_SEND_NOTIFICATION} from the service.
*
* @param userId The user id
* @param service The service's component name
*/
public void onNonA11yCategoryServiceUnbound(int userId, ComponentName service) {
mMainHandler.sendMessage(obtainMessage(this::cancelAlarm, userId, service));
}
private void setAlarm(int userId, ComponentName service) {
final Calendar cal = Calendar.getInstance();
cal.add(Calendar.HOUR, SEND_NOTIFICATION_DELAY_HOURS);
mAlarmManager.set(RTC_WAKEUP, cal.getTimeInMillis(),
createPendingIntent(mContext, userId, ACTION_SEND_NOTIFICATION, service));
}
private void cancelAlarm(int userId, ComponentName service) {
mAlarmManager.cancel(
createPendingIntent(mContext, userId, ACTION_SEND_NOTIFICATION, service));
}
protected static PendingIntent createPendingIntent(Context context, int userId, String action,
ComponentName serviceComponentName) {
return PendingIntent.getBroadcast(context, 0,
createIntent(context, userId, action, serviceComponentName),
PendingIntent.FLAG_IMMUTABLE);
}
protected static Intent createIntent(Context context, int userId, String action,
ComponentName serviceComponentName) {
final Intent intent = new Intent(action);
intent.setPackage(context.getPackageName())
.setIdentifier(serviceComponentName.flattenToShortString())
.putExtra(Intent.EXTRA_COMPONENT_NAME, serviceComponentName)
.putExtra(Intent.EXTRA_USER_ID, userId)
.putExtra(Intent.EXTRA_TIME, SystemClock.elapsedRealtime());
return intent;
}
/**
* Enables to send the notification for non-Accessibility services.
*/
public void enableSendingNonA11yToolNotification(boolean enable) {
mMainHandler.sendMessage(
obtainMessage(this::enableSendingNonA11yToolNotificationInternal, enable));
}
private void enableSendingNonA11yToolNotificationInternal(boolean enable) {
mNotificationController.setSendingNotification(enable);
}
/** A sub class to handle notifications and settings on the main thread. */
@MainThread
public static class NotificationController extends BroadcastReceiver {
private static final char RECORD_SEPARATOR = ':';
/** All accessibility services which are notified to the user by the policy warning rule. */
private final ArraySet<ComponentName> mNotifiedA11yServices = new ArraySet<>();
/** The component name of sent notifications. */
private final List<ComponentName> mSentA11yServiceNotification = new ArrayList<>();
private final NotificationManager mNotificationManager;
private final Context mContext;
private int mCurrentUserId;
private boolean mSendNotification;
public NotificationController(Context context) {
mContext = context;
mNotificationManager = mContext.getSystemService(NotificationManager.class);
}
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
final ComponentName componentName = intent.getParcelableExtra(
Intent.EXTRA_COMPONENT_NAME, android.content.ComponentName.class);
if (TextUtils.isEmpty(action) || componentName == null) {
return;
}
final long startTimeMills = intent.getLongExtra(Intent.EXTRA_TIME, 0);
final long durationMills =
startTimeMills > 0 ? SystemClock.elapsedRealtime() - startTimeMills : 0;
final int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_SYSTEM);
if (ACTION_SEND_NOTIFICATION.equals(action)) {
if (trySendNotification(userId, componentName)) {
AccessibilityStatsLogUtils.logNonA11yToolServiceWarningReported(
componentName.getPackageName(),
AccessibilityStatsLogUtils.ACCESSIBILITY_PRIVACY_WARNING_STATUS_SHOWN,
durationMills);
}
} else if (ACTION_A11Y_SETTINGS.equals(action)) {
if (tryLaunchSettings(userId, componentName)) {
AccessibilityStatsLogUtils.logNonA11yToolServiceWarningReported(
componentName.getPackageName(),
AccessibilityStatsLogUtils.ACCESSIBILITY_PRIVACY_WARNING_STATUS_CLICKED,
durationMills);
}
mNotificationManager.cancel(componentName.flattenToShortString(),
NOTE_A11Y_VIEW_AND_CONTROL_ACCESS);
mSentA11yServiceNotification.remove(componentName);
onNotificationCanceled(userId, componentName);
} else if (ACTION_DISMISS_NOTIFICATION.equals(action)) {
mSentA11yServiceNotification.remove(componentName);
onNotificationCanceled(userId, componentName);
}
}
protected void onSwitchUser(int userId) {
cancelSentNotifications();
mNotifiedA11yServices.clear();
mCurrentUserId = userId;
mNotifiedA11yServices.addAll(readNotifiedServiceList(userId));
}
protected void onServicesDisabled(int userId,
ArraySet<ComponentName> disabledServices) {
if (mNotifiedA11yServices.removeAll(disabledServices)) {
writeNotifiedServiceList(userId, mNotifiedA11yServices);
}
}
private boolean trySendNotification(int userId, ComponentName componentName) {
if (userId != mCurrentUserId) {
return false;
}
if (!mSendNotification) {
return false;
}
List<AccessibilityServiceInfo> enabledServiceInfos = getEnabledServiceInfos();
for (int i = 0; i < enabledServiceInfos.size(); i++) {
final AccessibilityServiceInfo a11yServiceInfo = enabledServiceInfos.get(i);
if (componentName.flattenToShortString().equals(
a11yServiceInfo.getComponentName().flattenToShortString())) {
if (!a11yServiceInfo.isAccessibilityTool()
&& !mNotifiedA11yServices.contains(componentName)) {
final CharSequence displayName =
a11yServiceInfo.getResolveInfo().serviceInfo.loadLabel(
mContext.getPackageManager());
final Drawable drawable = a11yServiceInfo.getResolveInfo().loadIcon(
mContext.getPackageManager());
final int size = mContext.getResources().getDimensionPixelSize(
android.R.dimen.app_icon_size);
sendNotification(userId, componentName, displayName,
ImageUtils.buildScaledBitmap(drawable, size, size));
return true;
}
break;
}
}
return false;
}
private boolean tryLaunchSettings(int userId, ComponentName componentName) {
if (userId != mCurrentUserId) {
return false;
}
final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName.flattenToShortString());
intent.putExtra(EXTRA_TIME_FOR_LOGGING, SystemClock.elapsedRealtime());
final Bundle bundle = ActivityOptions.makeBasic().setLaunchDisplayId(
mContext.getDisplayId()).toBundle();
mContext.startActivityAsUser(intent, bundle, UserHandle.of(userId));
mContext.getSystemService(StatusBarManager.class).collapsePanels();
return true;
}
protected void onNotificationCanceled(int userId, ComponentName componentName) {
if (userId != mCurrentUserId) {
return;
}
if (mNotifiedA11yServices.add(componentName)) {
writeNotifiedServiceList(userId, mNotifiedA11yServices);
}
}
private void sendNotification(int userId, ComponentName serviceComponentName,
CharSequence name,
Bitmap bitmap) {
final Notification.Builder notificationBuilder = new Notification.Builder(mContext,
SystemNotificationChannels.ACCESSIBILITY_SECURITY_POLICY);
notificationBuilder.setSmallIcon(R.drawable.ic_accessibility_24dp)
.setContentTitle(
mContext.getString(R.string.view_and_control_notification_title))
.setContentText(
mContext.getString(R.string.view_and_control_notification_content,
name))
.setStyle(new Notification.BigTextStyle()
.bigText(
mContext.getString(
R.string.view_and_control_notification_content,
name)))
.setTicker(mContext.getString(R.string.view_and_control_notification_title))
.setOnlyAlertOnce(true)
.setDeleteIntent(
createPendingIntent(mContext, userId, ACTION_DISMISS_NOTIFICATION,
serviceComponentName))
.setContentIntent(
createPendingIntent(mContext, userId, ACTION_A11Y_SETTINGS,
serviceComponentName));
if (bitmap != null) {
notificationBuilder.setLargeIcon(bitmap);
}
mNotificationManager.notify(serviceComponentName.flattenToShortString(),
NOTE_A11Y_VIEW_AND_CONTROL_ACCESS,
notificationBuilder.build());
mSentA11yServiceNotification.add(serviceComponentName);
}
private ArraySet<ComponentName> readNotifiedServiceList(int userId) {
final String notifiedServiceSetting = Settings.Secure.getStringForUser(
mContext.getContentResolver(),
Settings.Secure.NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES,
userId);
if (TextUtils.isEmpty(notifiedServiceSetting)) {
return new ArraySet<>();
}
final TextUtils.StringSplitter componentNameSplitter =
new TextUtils.SimpleStringSplitter(RECORD_SEPARATOR);
componentNameSplitter.setString(notifiedServiceSetting);
final ArraySet<ComponentName> notifiedServices = new ArraySet<>();
final Iterator<String> it = componentNameSplitter.iterator();
while (it.hasNext()) {
final String componentNameString = it.next();
final ComponentName notifiedService = ComponentName.unflattenFromString(
componentNameString);
if (notifiedService != null) {
notifiedServices.add(notifiedService);
}
}
return notifiedServices;
}
private void writeNotifiedServiceList(int userId, ArraySet<ComponentName> services) {
StringBuilder notifiedServicesBuilder = new StringBuilder();
for (int i = 0; i < services.size(); i++) {
if (i > 0) {
notifiedServicesBuilder.append(RECORD_SEPARATOR);
}
final ComponentName notifiedService = services.valueAt(i);
notifiedServicesBuilder.append(notifiedService.flattenToShortString());
}
Settings.Secure.putStringForUser(mContext.getContentResolver(),
Settings.Secure.NOTIFIED_NON_ACCESSIBILITY_CATEGORY_SERVICES,
notifiedServicesBuilder.toString(), userId);
}
@VisibleForTesting
protected List<AccessibilityServiceInfo> getEnabledServiceInfos() {
final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(
mContext);
return accessibilityManager.getEnabledAccessibilityServiceList(
AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
}
private void cancelSentNotifications() {
mSentA11yServiceNotification.forEach(componentName -> mNotificationManager.cancel(
componentName.flattenToShortString(), NOTE_A11Y_VIEW_AND_CONTROL_ACCESS));
mSentA11yServiceNotification.clear();
}
void setSendingNotification(boolean enable) {
mSendNotification = enable;
}
}
}