blob: 24eb304bf435e93e7e07b4fec88b7e1a393610c8 [file] [log] [blame]
/*
* Copyright (C) 2022 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.safetycenter;
import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.android.safetycenter.internaldata.SafetyCenterIds.toUserFriendlyString;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Binder;
import android.os.UserHandle;
import android.safetycenter.SafetySourceIssue;
import android.safetycenter.config.SafetySource;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.modules.utils.build.SdkLevel;
import com.android.permission.util.UserUtils;
import com.android.safetycenter.data.SafetyCenterDataManager;
import com.android.safetycenter.internaldata.SafetyCenterIds;
import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
import com.android.safetycenter.logging.SafetyCenterStatsdLogger;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Class responsible for posting, updating and dismissing Safety Center notifications each time
* Safety Center's issues change.
*
* <p>This class isn't thread safe. Thread safety must be handled by the caller.
*/
@RequiresApi(TIRAMISU)
@NotThreadSafe
final class SafetyCenterNotificationSender {
private static final String TAG = "SafetyCenterNS";
// We use a fixed notification ID because notifications are keyed by (tag, id) and it easier
// to differentiate our notifications using the tag
private static final int FIXED_NOTIFICATION_ID = 2345;
private static final int NOTIFICATION_BEHAVIOR_INTERNAL_NEVER = 100;
private static final int NOTIFICATION_BEHAVIOR_INTERNAL_DELAYED = 200;
private static final int NOTIFICATION_BEHAVIOR_INTERNAL_IMMEDIATELY = 300;
/**
* Internal notification behavior {@code @IntDef} which is related to the {@code
* SafetySourceIssue.NotificationBehavior} type introduced in Android U.
*
* <p>This definition is available on T+.
*
* <p>Unlike the U+/external {@code @IntDef}, this one has no "unspecified behavior" value. Any
* issues which have unspecified behavior are resolved to one of these specific behaviors based
* on their other properties.
*/
@IntDef(
prefix = {"NOTIFICATION_BEHAVIOR_INTERNAL"},
value = {
NOTIFICATION_BEHAVIOR_INTERNAL_NEVER,
NOTIFICATION_BEHAVIOR_INTERNAL_DELAYED,
NOTIFICATION_BEHAVIOR_INTERNAL_IMMEDIATELY
})
@Retention(RetentionPolicy.SOURCE)
private @interface NotificationBehaviorInternal {}
private final Context mContext;
private final SafetyCenterNotificationFactory mNotificationFactory;
private final SafetyCenterDataManager mSafetyCenterDataManager;
private final ArrayMap<SafetyCenterIssueKey, SafetySourceIssue> mNotifiedIssues =
new ArrayMap<>();
SafetyCenterNotificationSender(
Context context,
SafetyCenterNotificationFactory notificationFactory,
SafetyCenterDataManager safetyCenterDataManager) {
mContext = context;
mNotificationFactory = notificationFactory;
mSafetyCenterDataManager = safetyCenterDataManager;
}
/** Updates Safety Center notifications for the given {@link UserProfileGroup}. */
void updateNotifications(UserProfileGroup userProfileGroup) {
updateNotifications(userProfileGroup.getProfileParentUserId());
int[] managedProfileUserIds = userProfileGroup.getManagedProfilesUserIds();
for (int i = 0; i < managedProfileUserIds.length; i++) {
updateNotifications(managedProfileUserIds[i]);
}
}
/**
* Updates Safety Center notifications, usually in response to a change in the issues for the
* given userId.
*/
// TODO(b/266819195): Make sure to do the right thing if this method is called for a userId
// which is not running. We might want to update data related to it, but not send
// the notification...
void updateNotifications(@UserIdInt int userId) {
if (!SafetyCenterFlags.getNotificationsEnabled()) {
return;
}
NotificationManager notificationManager = getNotificationManagerForUser(userId);
if (notificationManager == null) {
Log.w(TAG, "Could not retrieve NotificationManager for user " + userId);
return;
}
ArrayMap<SafetyCenterIssueKey, SafetySourceIssue> issuesToNotify =
getIssuesToNotify(userId);
// Post or update notifications for notifiable issues. We keep track of the "fresh" issues
// keys of those issues which were just notified because doing so allows us to cancel any
// notifications for other, non-fresh issues.
ArraySet<SafetyCenterIssueKey> freshIssueKeys = new ArraySet<>();
for (int i = 0; i < issuesToNotify.size(); i++) {
SafetyCenterIssueKey issueKey = issuesToNotify.keyAt(i);
SafetySourceIssue issue = issuesToNotify.valueAt(i);
boolean unchanged = issue.equals(mNotifiedIssues.get(issueKey));
if (unchanged) {
freshIssueKeys.add(issueKey);
continue;
}
boolean wasPosted = postNotificationForIssue(notificationManager, issue, issueKey);
if (wasPosted) {
freshIssueKeys.add(issueKey);
}
}
cancelStaleNotifications(notificationManager, userId, freshIssueKeys);
}
/** Cancels all notifications previously posted by this class */
void cancelAllNotifications() {
// Loop in reverse index order to be able to remove entries while iterating
for (int i = mNotifiedIssues.size() - 1; i >= 0; i--) {
SafetyCenterIssueKey issueKey = mNotifiedIssues.keyAt(i);
cancelNotificationFromSystem(
getNotificationManagerForUser(issueKey.getUserId()),
getNotificationTag(issueKey));
mNotifiedIssues.removeAt(i);
}
}
/** Dumps state for debugging purposes. */
void dump(PrintWriter fout) {
int notifiedIssuesCount = mNotifiedIssues.size();
fout.println("NOTIFICATION SENDER (" + notifiedIssuesCount + " notified issues)");
for (int i = 0; i < notifiedIssuesCount; i++) {
SafetyCenterIssueKey key = mNotifiedIssues.keyAt(i);
SafetySourceIssue issue = mNotifiedIssues.valueAt(i);
fout.println("\t[" + i + "] " + toUserFriendlyString(key) + " -> " + issue);
}
fout.println();
}
/** Get all of the key-issue pairs for which notifications should be posted or updated now. */
private ArrayMap<SafetyCenterIssueKey, SafetySourceIssue> getIssuesToNotify(
@UserIdInt int userId) {
ArrayMap<SafetyCenterIssueKey, SafetySourceIssue> result = new ArrayMap<>();
List<SafetySourceIssueInfo> allIssuesInfo =
mSafetyCenterDataManager.getIssuesForUser(userId);
for (int i = 0; i < allIssuesInfo.size(); i++) {
SafetySourceIssueInfo issueInfo = allIssuesInfo.get(i);
SafetyCenterIssueKey issueKey = issueInfo.getSafetyCenterIssueKey();
SafetySourceIssue issue = issueInfo.getSafetySourceIssue();
if (!areNotificationsAllowedForSource(issueInfo.getSafetySource())) {
continue;
}
if (mSafetyCenterDataManager.isNotificationDismissedNow(
issueKey, issue.getSeverityLevel())) {
continue;
}
// Get the notification behavior for this issue which determines whether we should
// send a notification about it now
int behavior = getBehavior(issue, issueKey);
if (behavior == NOTIFICATION_BEHAVIOR_INTERNAL_IMMEDIATELY) {
result.put(issueKey, issue);
} else if (behavior == NOTIFICATION_BEHAVIOR_INTERNAL_DELAYED) {
if (canNotifyDelayedIssueNow(issueKey)) {
result.put(issueKey, issue);
}
// TODO(b/259094736): else handle delayed notifications using a scheduled job
}
}
return result;
}
@NotificationBehaviorInternal
private int getBehavior(SafetySourceIssue issue, SafetyCenterIssueKey issueKey) {
if (SdkLevel.isAtLeastU()) {
switch (issue.getNotificationBehavior()) {
case SafetySourceIssue.NOTIFICATION_BEHAVIOR_NEVER:
return NOTIFICATION_BEHAVIOR_INTERNAL_NEVER;
case SafetySourceIssue.NOTIFICATION_BEHAVIOR_DELAYED:
return NOTIFICATION_BEHAVIOR_INTERNAL_DELAYED;
case SafetySourceIssue.NOTIFICATION_BEHAVIOR_IMMEDIATELY:
return NOTIFICATION_BEHAVIOR_INTERNAL_IMMEDIATELY;
}
}
// On Android T all issues are assumed to have "unspecified" behavior
return getBehaviorForIssueWithUnspecifiedBehavior(issue, issueKey);
}
@NotificationBehaviorInternal
private int getBehaviorForIssueWithUnspecifiedBehavior(
SafetySourceIssue issue, SafetyCenterIssueKey issueKey) {
String flagKey = issueKey.getSafetySourceId() + "/" + issue.getIssueTypeId();
if (SafetyCenterFlags.getImmediateNotificationBehaviorIssues().contains(flagKey)) {
return NOTIFICATION_BEHAVIOR_INTERNAL_IMMEDIATELY;
} else {
return NOTIFICATION_BEHAVIOR_INTERNAL_NEVER;
}
}
private boolean areNotificationsAllowedForSource(SafetySource safetySource) {
if (SdkLevel.isAtLeastU()) {
if (safetySource.areNotificationsAllowed()) {
return true;
}
}
return SafetyCenterFlags.getNotificationsAllowedSourceIds().contains(safetySource.getId());
}
private boolean canNotifyDelayedIssueNow(SafetyCenterIssueKey issueKey) {
Duration minNotificationsDelay = SafetyCenterFlags.getNotificationsMinDelay();
Instant threshold = Instant.now().minus(minNotificationsDelay);
return mSafetyCenterDataManager.getIssueFirstSeenAt(issueKey).isBefore(threshold);
}
private boolean postNotificationForIssue(
NotificationManager notificationManager,
SafetySourceIssue issue,
SafetyCenterIssueKey key) {
Notification notification =
mNotificationFactory.newNotificationForIssue(notificationManager, issue, key);
if (notification == null) {
return false;
}
String tag = getNotificationTag(key);
boolean wasPosted = notifyFromSystem(notificationManager, tag, notification);
if (wasPosted) {
mNotifiedIssues.put(key, issue);
SafetyCenterStatsdLogger.writeNotificationPostedEvent(
key.getSafetySourceId(),
UserUtils.isManagedProfile(key.getUserId(), mContext),
issue.getIssueTypeId(),
issue.getSeverityLevel());
}
return wasPosted;
}
private void cancelStaleNotifications(
NotificationManager notificationManager,
@UserIdInt int userId,
ArraySet<SafetyCenterIssueKey> freshIssueKeys) {
// Loop in reverse index order to be able to remove entries while iterating
for (int i = mNotifiedIssues.size() - 1; i >= 0; i--) {
SafetyCenterIssueKey key = mNotifiedIssues.keyAt(i);
if (key.getUserId() == userId && !freshIssueKeys.contains(key)) {
String tag = getNotificationTag(key);
cancelNotificationFromSystem(notificationManager, tag);
mNotifiedIssues.removeAt(i);
}
}
}
private static String getNotificationTag(SafetyCenterIssueKey issueKey) {
// Base 64 encoding of the issueKey proto:
return SafetyCenterIds.encodeToString(issueKey);
}
/** Returns a {@link NotificationManager} which will send notifications to the given user. */
@Nullable
private NotificationManager getNotificationManagerForUser(@UserIdInt int userId) {
return SafetyCenterNotificationChannels.getNotificationManagerForUser(
mContext, UserHandle.of(userId));
}
/**
* Sends a {@link Notification} from the system, dropping any calling identity. Returns {@code
* true} if successful or {@code false} otherwise.
*
* <p>The recipient of the notification depends on the {@link Context} of the given {@link
* NotificationManager}. Use {@link #getNotificationManagerForUser(int)} to send notifications
* to a specific user.
*/
private boolean notifyFromSystem(
NotificationManager notificationManager,
@Nullable String tag,
Notification notification) {
// This call is needed to send a notification from the system and this also grants the
// necessary POST_NOTIFICATIONS permission.
final long callingId = Binder.clearCallingIdentity();
try {
// The fixed notification ID is OK because notifications are keyed by (tag, id)
notificationManager.notify(tag, FIXED_NOTIFICATION_ID, notification);
return true;
} catch (Throwable e) {
Log.w(TAG, "Unable to send system notification", e);
return false;
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
/**
* Cancels a {@link Notification} from the system, dropping any calling identity.
*
* <p>The recipient of the notification depends on the {@link Context} of the given {@link
* NotificationManager}. Use {@link #getNotificationManagerForUser(int)} to cancel notifications
* sent to a specific user.
*/
private void cancelNotificationFromSystem(
NotificationManager notificationManager, @Nullable String tag) {
// This call is needed to cancel a notification previously sent from the system
final long callingId = Binder.clearCallingIdentity();
try {
notificationManager.cancel(tag, FIXED_NOTIFICATION_ID);
} catch (Throwable e) {
Log.w(TAG, "Unable to cancel system notification", e);
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}