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