1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.safetycenter.notifications; 18 19 import static android.os.Build.VERSION_CODES.TIRAMISU; 20 21 import android.annotation.Nullable; 22 import android.app.PendingIntent; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.safetycenter.SafetySourceIssue; 28 import android.util.Log; 29 30 import androidx.annotation.RequiresApi; 31 32 import com.android.internal.annotations.GuardedBy; 33 import com.android.permission.util.UserUtils; 34 import com.android.safetycenter.ApiLock; 35 import com.android.safetycenter.PendingIntentFactory; 36 import com.android.safetycenter.SafetyCenterDataChangeNotifier; 37 import com.android.safetycenter.SafetyCenterFlags; 38 import com.android.safetycenter.SafetyCenterService; 39 import com.android.safetycenter.UserProfileGroup; 40 import com.android.safetycenter.data.SafetyCenterDataManager; 41 import com.android.safetycenter.internaldata.SafetyCenterIds; 42 import com.android.safetycenter.internaldata.SafetyCenterIssueActionId; 43 import com.android.safetycenter.internaldata.SafetyCenterIssueKey; 44 import com.android.safetycenter.logging.SafetyCenterStatsdLogger; 45 46 /** 47 * A Context-registered {@link BroadcastReceiver} that handles intents sent via Safety Center 48 * notifications e.g. when a notification is dismissed. 49 * 50 * <p>Use {@link #register(Context)} to register this receiver with the correct {@link IntentFilter} 51 * and use the {@link #newNotificationDismissedIntent(Context, SafetyCenterIssueKey)} and {@link 52 * #newNotificationActionClickedIntent(Context, SafetyCenterIssueActionId)} factory methods to 53 * create new {@link PendingIntent} instances for this receiver. 54 * 55 * @hide 56 */ 57 @RequiresApi(TIRAMISU) 58 public final class SafetyCenterNotificationReceiver extends BroadcastReceiver { 59 60 private static final String TAG = "SafetyCenterNR"; 61 62 private static final String ACTION_NOTIFICATION_DISMISSED = 63 "com.android.safetycenter.action.NOTIFICATION_DISMISSED"; 64 private static final String ACTION_NOTIFICATION_ACTION_CLICKED = 65 "com.android.safetycenter.action.NOTIFICATION_ACTION_CLICKED"; 66 private static final String EXTRA_ISSUE_KEY = "com.android.safetycenter.extra.ISSUE_KEY"; 67 private static final String EXTRA_ISSUE_ACTION_ID = 68 "com.android.safetycenter.extra.ISSUE_ACTION_ID"; 69 70 private static final int REQUEST_CODE_UNUSED = 0; 71 72 /** 73 * Creates a broadcast {@code PendingIntent} for this receiver which will handle a Safety Center 74 * notification being dismissed. 75 */ newNotificationDismissedIntent( Context context, SafetyCenterIssueKey issueKey)76 static PendingIntent newNotificationDismissedIntent( 77 Context context, SafetyCenterIssueKey issueKey) { 78 String issueKeyString = SafetyCenterIds.encodeToString(issueKey); 79 Intent intent = new Intent(ACTION_NOTIFICATION_DISMISSED); 80 intent.putExtra(EXTRA_ISSUE_KEY, issueKeyString); 81 intent.setIdentifier(issueKeyString); 82 int flags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT; 83 return PendingIntentFactory.getNonProtectedSystemOnlyBroadcastPendingIntent( 84 context, REQUEST_CODE_UNUSED, intent, flags); 85 } 86 87 /** 88 * Creates a broadcast {@code PendingIntent} for this receiver which will handle a Safety Center 89 * notification action being clicked. 90 * 91 * <p>Safety Center notification actions correspond to Safety Center issue actions. 92 */ newNotificationActionClickedIntent( Context context, SafetyCenterIssueActionId issueActionId)93 static PendingIntent newNotificationActionClickedIntent( 94 Context context, SafetyCenterIssueActionId issueActionId) { 95 String issueActionIdString = SafetyCenterIds.encodeToString(issueActionId); 96 Intent intent = new Intent(ACTION_NOTIFICATION_ACTION_CLICKED); 97 intent.putExtra(EXTRA_ISSUE_ACTION_ID, issueActionIdString); 98 intent.setIdentifier(issueActionIdString); 99 int flags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT; 100 return PendingIntentFactory.getNonProtectedSystemOnlyBroadcastPendingIntent( 101 context, REQUEST_CODE_UNUSED, intent, flags); 102 } 103 104 @Nullable getIssueKeyExtra(Intent intent)105 private static SafetyCenterIssueKey getIssueKeyExtra(Intent intent) { 106 String issueKeyString = intent.getStringExtra(EXTRA_ISSUE_KEY); 107 if (issueKeyString == null) { 108 Log.w(TAG, "Received notification dismissed broadcast with null issue key extra"); 109 return null; 110 } 111 try { 112 return SafetyCenterIds.issueKeyFromString(issueKeyString); 113 } catch (IllegalArgumentException e) { 114 Log.w(TAG, "Could not decode the issue key extra", e); 115 return null; 116 } 117 } 118 119 @Nullable getIssueActionIdExtra(Intent intent)120 private static SafetyCenterIssueActionId getIssueActionIdExtra(Intent intent) { 121 String issueActionIdString = intent.getStringExtra(EXTRA_ISSUE_ACTION_ID); 122 if (issueActionIdString == null) { 123 Log.w(TAG, "Received notification action broadcast with null issue action ID"); 124 return null; 125 } 126 try { 127 return SafetyCenterIds.issueActionIdFromString(issueActionIdString); 128 } catch (IllegalArgumentException e) { 129 Log.w(TAG, "Could not decode the issue action ID", e); 130 return null; 131 } 132 } 133 134 private final SafetyCenterService mService; 135 136 @GuardedBy("mApiLock") 137 private final SafetyCenterDataManager mSafetyCenterDataManager; 138 139 @GuardedBy("mApiLock") 140 private final SafetyCenterDataChangeNotifier mSafetyCenterDataChangeNotifier; 141 142 private final ApiLock mApiLock; 143 SafetyCenterNotificationReceiver( SafetyCenterService service, SafetyCenterDataManager safetyCenterDataManager, SafetyCenterDataChangeNotifier safetyCenterDataChangeNotifier, ApiLock apiLock)144 public SafetyCenterNotificationReceiver( 145 SafetyCenterService service, 146 SafetyCenterDataManager safetyCenterDataManager, 147 SafetyCenterDataChangeNotifier safetyCenterDataChangeNotifier, 148 ApiLock apiLock) { 149 mService = service; 150 mSafetyCenterDataManager = safetyCenterDataManager; 151 mSafetyCenterDataChangeNotifier = safetyCenterDataChangeNotifier; 152 mApiLock = apiLock; 153 } 154 155 /** 156 * Register this receiver in the given {@link Context} with an {@link IntentFilter} that matches 157 * any intents created by this class' static factory methods. 158 * 159 * @see #newNotificationDismissedIntent(Context, SafetyCenterIssueKey) 160 */ register(Context context)161 public void register(Context context) { 162 IntentFilter filter = new IntentFilter(); 163 filter.addAction(ACTION_NOTIFICATION_DISMISSED); 164 filter.addAction(ACTION_NOTIFICATION_ACTION_CLICKED); 165 context.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED); 166 } 167 168 @Override onReceive(Context context, Intent intent)169 public void onReceive(Context context, Intent intent) { 170 if (!SafetyCenterFlags.getSafetyCenterEnabled() 171 || !SafetyCenterFlags.getNotificationsEnabled()) { 172 return; 173 } 174 175 Log.d(TAG, "Received broadcast with action " + intent.getAction()); 176 String action = intent.getAction(); 177 if (action == null) { 178 Log.w(TAG, "Received broadcast with null action!"); 179 return; 180 } 181 182 switch (action) { 183 case ACTION_NOTIFICATION_DISMISSED: 184 onNotificationDismissed(context, intent); 185 break; 186 case ACTION_NOTIFICATION_ACTION_CLICKED: 187 onNotificationActionClicked(context, intent); 188 break; 189 default: 190 Log.w(TAG, "Received broadcast with unrecognized action: " + action); 191 break; 192 } 193 } 194 onNotificationDismissed(Context context, Intent intent)195 private void onNotificationDismissed(Context context, Intent intent) { 196 SafetyCenterIssueKey issueKey = getIssueKeyExtra(intent); 197 if (issueKey == null) { 198 return; 199 } 200 201 int userId = issueKey.getUserId(); 202 UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(context, userId); 203 204 SafetySourceIssue dismissedIssue; 205 synchronized (mApiLock) { 206 dismissedIssue = mSafetyCenterDataManager.getSafetySourceIssue(issueKey); 207 mSafetyCenterDataManager.dismissNotification(issueKey); 208 mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroup, userId); 209 } 210 211 if (dismissedIssue != null) { 212 SafetyCenterStatsdLogger.writeNotificationDismissedEvent( 213 issueKey.getSafetySourceId(), 214 UserUtils.isManagedProfile(userId, context), 215 dismissedIssue.getIssueTypeId(), 216 dismissedIssue.getSeverityLevel()); 217 } 218 } 219 onNotificationActionClicked(Context context, Intent intent)220 private void onNotificationActionClicked(Context context, Intent intent) { 221 SafetyCenterIssueActionId issueActionId = getIssueActionIdExtra(intent); 222 if (issueActionId == null) { 223 return; 224 } 225 226 mService.executeIssueActionInternal(issueActionId); 227 logNotificationActionClicked(context, issueActionId); 228 } 229 logNotificationActionClicked( Context context, SafetyCenterIssueActionId issueActionId)230 private void logNotificationActionClicked( 231 Context context, SafetyCenterIssueActionId issueActionId) { 232 SafetyCenterIssueKey issueKey = issueActionId.getSafetyCenterIssueKey(); 233 SafetySourceIssue issue; 234 synchronized (mApiLock) { 235 issue = mSafetyCenterDataManager.getSafetySourceIssue(issueKey); 236 } 237 if (issue != null) { 238 SafetyCenterStatsdLogger.writeNotificationActionClickedEvent( 239 issueKey.getSafetySourceId(), 240 UserUtils.isManagedProfile(issueKey.getUserId(), context), 241 issue.getIssueTypeId(), 242 issue.getSeverityLevel(), 243 isPrimaryAction(issue, issueActionId)); 244 } 245 } 246 247 /** Returns {@code true} if {@code actionId} is the first action of {@code issue}. */ isPrimaryAction(SafetySourceIssue issue, SafetyCenterIssueActionId actionId)248 private boolean isPrimaryAction(SafetySourceIssue issue, SafetyCenterIssueActionId actionId) { 249 return !issue.getActions().isEmpty() 250 && issue.getActions() 251 .get(0) 252 .getId() 253 .equals(actionId.getSafetySourceIssueActionId()); 254 } 255 } 256