/* * Copyright (C) 2023 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.notifications; import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED; import static com.android.safetycenter.internaldata.SafetyCenterIds.toUserFriendlyString; import android.annotation.IntDef; 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.SafetyEvent; import android.safetycenter.SafetySourceIssue; import android.safetycenter.config.SafetySource; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import androidx.annotation.Nullable; import com.android.modules.utils.build.SdkLevel; import com.android.safetycenter.SafetyCenterFlags; import com.android.safetycenter.SafetySourceIssueInfo; import com.android.safetycenter.SafetySourceIssues; import com.android.safetycenter.UserProfileGroup; 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 com.android.safetycenter.resources.SafetyCenterResourcesApk; 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. * *
This class isn't thread safe. Thread safety must be handled by the caller. * * @hide */ @NotThreadSafe public 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. * *
This definition is available on T+. * *
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 The given {@link SafetyEvent} have type {@link
* SafetyEvent#SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED} and include issue and action IDs
* that correspond to a {@link SafetySourceIssue} for which a notification is currently
* displayed. Otherwise, this method has no effect.
*
* @param sourceId of the source which reported the issue
* @param safetyEvent the source provided upon successful action resolution
* @param userId to which the source, issue and notification belong
*/
public void notifyActionSuccess(
String sourceId, SafetyEvent safetyEvent, @UserIdInt int userId) {
if (!SafetyCenterFlags.getNotificationsEnabled()) {
// TODO(b/284271124): Decide what to do with existing notifications if flag gets
// toggled.
return;
}
if (safetyEvent.getType() != SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED) {
Log.w(TAG, "Received safety event of wrong type");
return;
}
String sourceIssueId = safetyEvent.getSafetySourceIssueId();
if (sourceIssueId == null) {
Log.w(TAG, "Received safety event without a safety source issue id");
return;
}
String sourceIssueActionId = safetyEvent.getSafetySourceIssueActionId();
if (sourceIssueActionId == null) {
Log.w(TAG, "Received safety event without a safety source issue action id");
return;
}
SafetyCenterIssueKey issueKey =
SafetyCenterIssueKey.newBuilder()
.setSafetySourceId(sourceId)
.setSafetySourceIssueId(sourceIssueId)
.setUserId(userId)
.build();
SafetySourceIssue notifiedIssue = mNotifiedIssues.get(issueKey);
if (notifiedIssue == null) {
Log.w(TAG, "No notification for this issue");
return;
}
SafetySourceIssue.Action successfulAction =
SafetySourceIssues.findAction(notifiedIssue, sourceIssueActionId);
if (successfulAction == null) {
Log.w(TAG, "Successful action not found");
return;
}
NotificationManager notificationManager = getNotificationManagerForUser(userId);
if (notificationManager == null) {
return;
}
Notification notification =
mNotificationFactory.newNotificationForSuccessfulAction(
notificationManager, notifiedIssue, successfulAction, userId);
if (notification == null) {
Log.w(TAG, "Could not create successful action notification");
return;
}
String tag = getNotificationTag(issueKey);
boolean wasPosted = notifyFromSystem(notificationManager, tag, notification);
if (wasPosted) {
// If the original issue notification was successfully replaced the key removed from
// mNotifiedIssues to prevent the success notification from being removed by
// cancelStaleNotifications below.
mNotifiedIssues.remove(issueKey);
}
}
/** Updates Safety Center notifications for the given {@link UserProfileGroup}. */
public void updateNotifications(UserProfileGroup userProfileGroup) {
int[] allProfilesUserIds = userProfileGroup.getAllProfilesUserIds();
for (int i = 0; i < allProfilesUserIds.length; i++) {
updateNotifications(allProfilesUserIds[i]);
}
}
/**
* Updates Safety Center notifications, usually in response to a change in the issues for the
* given userId.
*/
public void updateNotifications(@UserIdInt int userId) {
if (!SafetyCenterFlags.getNotificationsEnabled()) {
// TODO(b/284271124): Decide what to do with existing notifications
return;
}
NotificationManager notificationManager = getNotificationManagerForUser(userId);
if (notificationManager == null) {
return;
}
ArrayMap 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.
*
* 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);
}
}
}