/* * 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.SafetyCenterManager.EXTRA_SAFETY_SOURCE_ID; import static android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCE_ISSUE_ID; import static android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCE_USER_HANDLE; import static com.android.safetycenter.notifications.SafetyCenterNotificationChannels.getContextAsUser; import android.annotation.ColorInt; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.UserHandle; import android.safetycenter.SafetySourceData; import android.safetycenter.SafetySourceIssue; import android.text.TextUtils; import androidx.annotation.RequiresApi; import com.android.modules.utils.build.SdkLevel; import com.android.safetycenter.PendingIntentFactory; import com.android.safetycenter.internaldata.SafetyCenterIds; import com.android.safetycenter.internaldata.SafetyCenterIssueActionId; import com.android.safetycenter.internaldata.SafetyCenterIssueKey; import com.android.safetycenter.resources.SafetyCenterResourcesContext; import java.time.Duration; import java.util.List; /** * Factory that builds {@link Notification} objects from {@link SafetySourceIssue} instances with * appropriate {@link PendingIntent}s for click and dismiss callbacks. */ @RequiresApi(TIRAMISU) final class SafetyCenterNotificationFactory { private static final String TAG = "SafetyCenterNF"; private static final int OPEN_SAFETY_CENTER_REQUEST_CODE = 1221; private static final Duration SUCCESS_NOTIFICATION_TIMEOUT = Duration.ofSeconds(10); private final Context mContext; private final SafetyCenterNotificationChannels mNotificationChannels; private final SafetyCenterResourcesContext mResourcesContext; SafetyCenterNotificationFactory( Context context, SafetyCenterNotificationChannels notificationChannels, SafetyCenterResourcesContext resourcesContext) { mContext = context; mNotificationChannels = notificationChannels; mResourcesContext = resourcesContext; } /** * Creates and returns a new {@link Notification} for a successful action, or {@code null} if * none could be created. * *

The provided {@link NotificationManager} is used to create or update the {@link * NotificationChannel} for the notification. */ @Nullable Notification newNotificationForSuccessfulAction( NotificationManager notificationManager, SafetySourceIssue issue, SafetySourceIssue.Action action, @UserIdInt int userId) { String channelId = mNotificationChannels.getCreatedChannelId(notificationManager, issue); if (channelId == null) { return null; } PendingIntent contentIntent = newSafetyCenterPendingIntent(userId); if (contentIntent == null) { return null; } Notification.Builder builder = new Notification.Builder(mContext, channelId) .setSmallIcon( getNotificationIcon(SafetySourceData.SEVERITY_LEVEL_INFORMATION)) .setExtras(getNotificationExtras()) .setContentTitle(action.getSuccessMessage()) .setShowWhen(true) .setTimeoutAfter(SUCCESS_NOTIFICATION_TIMEOUT.toMillis()) .setContentIntent(contentIntent); Integer color = getNotificationColor(SafetySourceData.SEVERITY_LEVEL_INFORMATION); if (color != null) { builder.setColor(color); } return builder.build(); } /** * Creates and returns a new {@link Notification} instance corresponding to the given issue, or * {@code null} if none could be created. * *

The provided {@link NotificationManager} is used to create or update the {@link * NotificationChannel} for the notification. */ @Nullable Notification newNotificationForIssue( NotificationManager notificationManager, SafetySourceIssue issue, SafetyCenterIssueKey issueKey) { String channelId = mNotificationChannels.getCreatedChannelId(notificationManager, issue); if (channelId == null) { return null; } CharSequence title = issue.getTitle(); CharSequence text = issue.getSummary(); List issueActions = issue.getActions(); if (SdkLevel.isAtLeastU()) { SafetySourceIssue.Notification customNotification = issue.getCustomNotification(); if (customNotification != null) { title = customNotification.getTitle(); text = customNotification.getText(); issueActions = customNotification.getActions(); } } PendingIntent contentIntent = newSafetyCenterPendingIntent(issueKey); if (contentIntent == null) { return null; } Notification.Builder builder = new Notification.Builder(mContext, channelId) .setSmallIcon(getNotificationIcon(issue.getSeverityLevel())) .setExtras(getNotificationExtras()) .setShowWhen(true) .setContentTitle(title) .setContentText(text) .setContentIntent(contentIntent) .setDeleteIntent( SafetyCenterNotificationReceiver.newNotificationDismissedIntent( mContext, issueKey)); Integer color = getNotificationColor(issue.getSeverityLevel()); if (color != null) { builder.setColor(color); } for (int i = 0; i < issueActions.size(); i++) { Notification.Action notificationAction = toNotificationAction(issueKey, issueActions.get(i)); builder.addAction(notificationAction); } return builder.build(); } /** * Returns a {@link PendingIntent} to open Safety Center, navigating to a specific issue, or * {@code null} if no such intent can be created. */ @Nullable private PendingIntent newSafetyCenterPendingIntent(SafetyCenterIssueKey issueKey) { UserHandle userHandle = UserHandle.of(issueKey.getUserId()); Context userContext = getContextAsUser(mContext, userHandle); if (userContext == null) { return null; } Intent intent = newSafetyCenterIntent(); // Set the encoded issue key as the intent's identifier to ensure the PendingIntents of // different notifications do not collide: intent.setIdentifier(SafetyCenterIds.encodeToString(issueKey)); intent.putExtra(EXTRA_SAFETY_SOURCE_ID, issueKey.getSafetySourceId()); intent.putExtra(EXTRA_SAFETY_SOURCE_ISSUE_ID, issueKey.getSafetySourceIssueId()); intent.putExtra(EXTRA_SAFETY_SOURCE_USER_HANDLE, userHandle); return PendingIntentFactory.getActivityPendingIntent( userContext, OPEN_SAFETY_CENTER_REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE); } /** * Returns a {@link PendingIntent} to open Safety Center, or {@code null} if no such intent can * be created. */ @Nullable private PendingIntent newSafetyCenterPendingIntent(@UserIdInt int userId) { Context userContext = getContextAsUser(mContext, UserHandle.of(userId)); if (userContext == null) { return null; } return PendingIntentFactory.getActivityPendingIntent( userContext, OPEN_SAFETY_CENTER_REQUEST_CODE, newSafetyCenterIntent(), PendingIntent.FLAG_IMMUTABLE); } private static Intent newSafetyCenterIntent() { Intent intent = new Intent(Intent.ACTION_SAFETY_CENTER); // This extra is defined in the PermissionController APK, cannot be referenced directly: intent.putExtra("navigation_source_intent_extra", "NOTIFICATION"); return intent; } private Icon getNotificationIcon(@SafetySourceData.SeverityLevel int severityLevel) { String iconResName = "ic_notification_badge_general"; if (severityLevel == SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING) { iconResName = "ic_notification_badge_critical"; } Icon icon = mResourcesContext.getIconByDrawableName(iconResName); if (icon == null) { // In case it was impossible to fetch the above drawable for any reason use this // fallback which should be present on all Android devices: icon = Icon.createWithResource(mContext, android.R.drawable.ic_dialog_alert); } return icon; } @ColorInt @Nullable private Integer getNotificationColor(@SafetySourceData.SeverityLevel int severityLevel) { String colorResName = "notification_tint_normal"; if (severityLevel == SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING) { colorResName = "notification_tint_critical"; } return mResourcesContext.getColorByName(colorResName); } private Bundle getNotificationExtras() { Bundle extras = new Bundle(); String appName = mResourcesContext.getStringByName("notification_channel_group_name"); if (!TextUtils.isEmpty(appName)) { extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, appName); } return extras; } private Notification.Action toNotificationAction( SafetyCenterIssueKey issueKey, SafetySourceIssue.Action issueAction) { PendingIntent pendingIntent = getPendingIntentForAction(issueKey, issueAction); return new Notification.Action.Builder(null, issueAction.getLabel(), pendingIntent).build(); } private PendingIntent getPendingIntentForAction( SafetyCenterIssueKey issueKey, SafetySourceIssue.Action issueAction) { if (issueAction.willResolve()) { return getReceiverPendingIntentForResolvingAction(issueKey, issueAction); } else { return getDirectPendingIntentForNonResolvingAction(issueKey, issueAction); } } private PendingIntent getReceiverPendingIntentForResolvingAction( SafetyCenterIssueKey issueKey, SafetySourceIssue.Action issueAction) { // We do not use the action's PendingIntent directly here instead we build a new PI which // will be handled by our SafetyCenterNotificationReceiver which will in turn dispatch // the source-provided action PI. This ensures that action execution is consistent across // between Safety Center UI and notifications, for example executing an action from a // notification will send an "action in-flight" update to any current listeners. SafetyCenterIssueActionId issueActionId = SafetyCenterIssueActionId.newBuilder() .setSafetyCenterIssueKey(issueKey) .setSafetySourceIssueActionId(issueAction.getId()) .build(); return SafetyCenterNotificationReceiver.newNotificationActionClickedIntent( mContext, issueActionId); } private PendingIntent getDirectPendingIntentForNonResolvingAction( SafetyCenterIssueKey issueKey, SafetySourceIssue.Action issueAction) { return issueAction.getPendingIntent(); } private static boolean isDarkTheme(Context context) { return (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; } }