• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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