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 android.app.PendingIntent; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.safetycenter.SafetySourceIssue; 25 import android.util.Log; 26 27 import androidx.annotation.Nullable; 28 29 import com.android.internal.annotations.GuardedBy; 30 import com.android.safetycenter.ApiLock; 31 import com.android.safetycenter.PendingIntentFactory; 32 import com.android.safetycenter.SafetyCenterDataChangeNotifier; 33 import com.android.safetycenter.SafetyCenterFlags; 34 import com.android.safetycenter.SafetyCenterService; 35 import com.android.safetycenter.SafetySourceIssues; 36 import com.android.safetycenter.UserProfileGroup; 37 import com.android.safetycenter.data.SafetyCenterDataManager; 38 import com.android.safetycenter.internaldata.SafetyCenterIds; 39 import com.android.safetycenter.internaldata.SafetyCenterIssueActionId; 40 import com.android.safetycenter.internaldata.SafetyCenterIssueKey; 41 import com.android.safetycenter.logging.SafetyCenterStatsdLogger; 42 43 /** 44 * A Context-registered {@link BroadcastReceiver} that handles intents sent via Safety Center 45 * notifications e.g. when a notification is dismissed. 46 * 47 * <p>Use {@link #register(Context)} to register this receiver with the correct {@link IntentFilter} 48 * and use the {@link #newNotificationDismissedIntent(Context, SafetyCenterIssueKey)} and {@link 49 * #newNotificationActionClickedIntent(Context, SafetyCenterIssueActionId)} factory methods to 50 * create new {@link PendingIntent} instances for this receiver. 51 * 52 * @hide 53 */ 54 public final class SafetyCenterNotificationReceiver extends BroadcastReceiver { 55 56 private static final String TAG = "SafetyCenterNR"; 57 58 private static final String ACTION_NOTIFICATION_DISMISSED = 59 "com.android.safetycenter.action.NOTIFICATION_DISMISSED"; 60 private static final String ACTION_NOTIFICATION_ACTION_CLICKED = 61 "com.android.safetycenter.action.NOTIFICATION_ACTION_CLICKED"; 62 private static final String EXTRA_ISSUE_KEY = "com.android.safetycenter.extra.ISSUE_KEY"; 63 private static final String EXTRA_ISSUE_ACTION_ID = 64 "com.android.safetycenter.extra.ISSUE_ACTION_ID"; 65 66 private static final int REQUEST_CODE_UNUSED = 0; 67 68 /** 69 * Creates a broadcast {@code PendingIntent} for this receiver which will handle a Safety Center 70 * notification being dismissed. 71 */ newNotificationDismissedIntent( Context context, SafetyCenterIssueKey issueKey)72 static PendingIntent newNotificationDismissedIntent( 73 Context context, SafetyCenterIssueKey issueKey) { 74 String issueKeyString = SafetyCenterIds.encodeToString(issueKey); 75 Intent intent = new Intent(ACTION_NOTIFICATION_DISMISSED); 76 intent.putExtra(EXTRA_ISSUE_KEY, issueKeyString); 77 intent.setIdentifier(issueKeyString); 78 int flags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT; 79 return PendingIntentFactory.getNonProtectedSystemOnlyBroadcastPendingIntent( 80 context, REQUEST_CODE_UNUSED, intent, flags); 81 } 82 83 /** 84 * Creates a broadcast {@code PendingIntent} for this receiver which will handle a Safety Center 85 * notification action being clicked. 86 * 87 * <p>Safety Center notification actions correspond to Safety Center issue actions. 88 */ newNotificationActionClickedIntent( Context context, SafetyCenterIssueActionId issueActionId)89 static PendingIntent newNotificationActionClickedIntent( 90 Context context, SafetyCenterIssueActionId issueActionId) { 91 String issueActionIdString = SafetyCenterIds.encodeToString(issueActionId); 92 Intent intent = new Intent(ACTION_NOTIFICATION_ACTION_CLICKED); 93 intent.putExtra(EXTRA_ISSUE_ACTION_ID, issueActionIdString); 94 intent.setIdentifier(issueActionIdString); 95 int flags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT; 96 return PendingIntentFactory.getNonProtectedSystemOnlyBroadcastPendingIntent( 97 context, REQUEST_CODE_UNUSED, intent, flags); 98 } 99 100 @Nullable getIssueKeyExtra(Intent intent)101 private static SafetyCenterIssueKey getIssueKeyExtra(Intent intent) { 102 String issueKeyString = intent.getStringExtra(EXTRA_ISSUE_KEY); 103 if (issueKeyString == null) { 104 Log.w(TAG, "Received notification dismissed broadcast with null issue key extra"); 105 return null; 106 } 107 try { 108 return SafetyCenterIds.issueKeyFromString(issueKeyString); 109 } catch (IllegalArgumentException e) { 110 Log.w(TAG, "Could not decode the issue key extra", e); 111 return null; 112 } 113 } 114 115 @Nullable getIssueActionIdExtra(Intent intent)116 private static SafetyCenterIssueActionId getIssueActionIdExtra(Intent intent) { 117 String issueActionIdString = intent.getStringExtra(EXTRA_ISSUE_ACTION_ID); 118 if (issueActionIdString == null) { 119 Log.w(TAG, "Received notification action broadcast with null issue action id"); 120 return null; 121 } 122 try { 123 return SafetyCenterIds.issueActionIdFromString(issueActionIdString); 124 } catch (IllegalArgumentException e) { 125 Log.w(TAG, "Could not decode the issue action id", e); 126 return null; 127 } 128 } 129 130 private final SafetyCenterService mService; 131 132 @GuardedBy("mApiLock") 133 private final SafetyCenterDataManager mSafetyCenterDataManager; 134 135 @GuardedBy("mApiLock") 136 private final SafetyCenterDataChangeNotifier mSafetyCenterDataChangeNotifier; 137 138 private final ApiLock mApiLock; 139 SafetyCenterNotificationReceiver( SafetyCenterService service, SafetyCenterDataManager safetyCenterDataManager, SafetyCenterDataChangeNotifier safetyCenterDataChangeNotifier, ApiLock apiLock)140 public SafetyCenterNotificationReceiver( 141 SafetyCenterService service, 142 SafetyCenterDataManager safetyCenterDataManager, 143 SafetyCenterDataChangeNotifier safetyCenterDataChangeNotifier, 144 ApiLock apiLock) { 145 mService = service; 146 mSafetyCenterDataManager = safetyCenterDataManager; 147 mSafetyCenterDataChangeNotifier = safetyCenterDataChangeNotifier; 148 mApiLock = apiLock; 149 } 150 151 /** 152 * Register this receiver in the given {@link Context} with an {@link IntentFilter} that matches 153 * any intents created by this class' static factory methods. 154 * 155 * @see #newNotificationDismissedIntent(Context, SafetyCenterIssueKey) 156 */ register(Context context)157 public void register(Context context) { 158 IntentFilter filter = new IntentFilter(); 159 filter.addAction(ACTION_NOTIFICATION_DISMISSED); 160 filter.addAction(ACTION_NOTIFICATION_ACTION_CLICKED); 161 context.registerReceiver(/* receiver= */ this, filter, Context.RECEIVER_NOT_EXPORTED); 162 } 163 164 @Override onReceive(Context context, Intent intent)165 public void onReceive(Context context, Intent intent) { 166 if (!SafetyCenterFlags.getSafetyCenterEnabled()) { 167 Log.i(TAG, "Received notification broadcast but Safety Center is disabled"); 168 return; 169 } 170 171 if (!SafetyCenterFlags.getNotificationsEnabled()) { 172 // TODO(b/284271124): Decide what to do with existing notifications 173 Log.i(TAG, "Received notification broadcast but notifications are disabled"); 174 return; 175 } 176 177 String action = intent.getAction(); 178 if (action == null) { 179 Log.w(TAG, "Received broadcast with null action"); 180 return; 181 } 182 Log.d(TAG, "Received broadcast with action: " + action); 183 184 switch (action) { 185 case ACTION_NOTIFICATION_DISMISSED: 186 onNotificationDismissed(context, intent); 187 break; 188 case ACTION_NOTIFICATION_ACTION_CLICKED: 189 onNotificationActionClicked(context, intent); 190 break; 191 default: 192 Log.w(TAG, "Received broadcast with unrecognized action: " + action); 193 break; 194 } 195 } 196 onNotificationDismissed(Context context, Intent intent)197 private void onNotificationDismissed(Context context, Intent intent) { 198 SafetyCenterIssueKey issueKey = getIssueKeyExtra(intent); 199 if (issueKey == null) { 200 return; 201 } 202 203 int userId = issueKey.getUserId(); 204 UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(context, userId); 205 206 SafetySourceIssue dismissedIssue; 207 synchronized (mApiLock) { 208 dismissedIssue = mSafetyCenterDataManager.getSafetySourceIssue(issueKey); 209 mSafetyCenterDataManager.dismissNotification(issueKey); 210 mSafetyCenterDataChangeNotifier.updateDataConsumers(userProfileGroup, userId); 211 } 212 213 if (dismissedIssue != null) { 214 SafetyCenterStatsdLogger.writeNotificationDismissedEvent( 215 issueKey.getSafetySourceId(), 216 UserProfileGroup.getProfileTypeOfUser(userId, context), 217 dismissedIssue.getIssueTypeId(), 218 dismissedIssue.getSeverityLevel()); 219 } 220 } 221 onNotificationActionClicked(Context context, Intent intent)222 private void onNotificationActionClicked(Context context, Intent intent) { 223 SafetyCenterIssueActionId issueActionId = getIssueActionIdExtra(intent); 224 if (issueActionId == null) { 225 return; 226 } 227 228 mService.executeIssueActionInternal(issueActionId); 229 logNotificationActionClicked(context, issueActionId); 230 } 231 logNotificationActionClicked( Context context, SafetyCenterIssueActionId issueActionId)232 private void logNotificationActionClicked( 233 Context context, SafetyCenterIssueActionId issueActionId) { 234 SafetyCenterIssueKey issueKey = issueActionId.getSafetyCenterIssueKey(); 235 SafetySourceIssue issue; 236 synchronized (mApiLock) { 237 issue = mSafetyCenterDataManager.getSafetySourceIssue(issueKey); 238 } 239 if (issue != null) { 240 SafetyCenterStatsdLogger.writeNotificationActionClickedEvent( 241 issueKey.getSafetySourceId(), 242 UserProfileGroup.getProfileTypeOfUser(issueKey.getUserId(), context), 243 issue.getIssueTypeId(), 244 issue.getSeverityLevel(), 245 SafetySourceIssues.isPrimaryAction( 246 issue, issueActionId.getSafetySourceIssueActionId())); 247 } 248 } 249 } 250