/* * 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.os.Build.VERSION_CODES.TIRAMISU; 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.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.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.RequiresApi; import com.android.modules.utils.build.SdkLevel; import com.android.permission.util.UserUtils; import com.android.safetycenter.SafetyCenterFlags; import com.android.safetycenter.SafetySourceIssueInfo; 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.SafetyCenterResourcesContext; 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 */ @RequiresApi(TIRAMISU) @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 (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 = null;
        for (int i = 0; i < notifiedIssue.getActions().size(); i++) {
            if (notifiedIssue.getActions().get(i).getId().equals(sourceIssueActionId)) {
                successfulAction = notifiedIssue.getActions().get(i);
            }
        }
        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) {
        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.
     */
    public void updateNotifications(@UserIdInt int userId) {
        if (!SafetyCenterFlags.getNotificationsEnabled()) {
            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);
        }
    }
}