1 /*
2  * Copyright (C) 2018 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.car.notification;
18 
19 import android.annotation.Nullable;
20 import android.app.ActivityManager;
21 import android.app.Notification;
22 import android.app.PendingIntent;
23 import android.app.RemoteInput;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.os.Bundle;
27 import android.os.RemoteException;
28 import android.service.notification.NotificationStats;
29 import android.util.Log;
30 import android.view.View;
31 import android.view.WindowManager;
32 import android.widget.Button;
33 import android.widget.Toast;
34 
35 import com.android.car.assist.CarVoiceInteractionSession;
36 import com.android.car.assist.client.CarAssistUtils;
37 import com.android.internal.statusbar.IStatusBarService;
38 import com.android.internal.statusbar.NotificationVisibility;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 /**
44  * Factory that builds a {@link View.OnClickListener} to handle the logic of what to do when a
45  * notification is clicked. It also handles the interaction with the StatusBarService.
46  */
47 public class NotificationClickHandlerFactory {
48 
49     /**
50      * Callback that will be issued after a notification is clicked.
51      */
52     public interface OnNotificationClickListener {
53 
54         /**
55          * A notification was clicked and handleNotificationClicked was invoked.
56          *
57          * @param launchResult For non-Assistant actions, returned from
58          *        {@link PendingIntent#sendAndReturnResult}; for Assistant actions,
59          *        returns {@link ActivityManager#START_SUCCESS} on success;
60          *        {@link ActivityManager#START_ABORTED} otherwise.
61          *
62          * @param alertEntry {@link AlertEntry} whose Notification was clicked.
63          */
onNotificationClicked(int launchResult, AlertEntry alertEntry)64         void onNotificationClicked(int launchResult, AlertEntry alertEntry);
65     }
66 
67     private static final String TAG = "NotificationClickHandlerFactory";
68 
69     private final IStatusBarService mBarService;
70     private final List<OnNotificationClickListener> mClickListeners = new ArrayList<>();
71     private CarAssistUtils mCarAssistUtils;
72     @Nullable
73     private NotificationDataManager mNotificationDataManager;
74 
NotificationClickHandlerFactory(IStatusBarService barService)75     public NotificationClickHandlerFactory(IStatusBarService barService) {
76         mBarService = barService;
77         mCarAssistUtils = null;
78     }
79 
80     /**
81      * Sets the {@link NotificationDataManager} which contains additional state information of the
82      * {@link AlertEntry}s.
83      */
setNotificationDataManager(NotificationDataManager manager)84     public void setNotificationDataManager(NotificationDataManager manager) {
85         mNotificationDataManager = manager;
86     }
87 
88     /**
89      * Returns the {@link NotificationDataManager} which contains additional state information of
90      * the {@link AlertEntry}s.
91      */
92     @Nullable
getNotificationDataManager()93     public NotificationDataManager getNotificationDataManager() {
94         return mNotificationDataManager;
95     }
96 
97     /**
98      * Returns a {@link View.OnClickListener} that should be used for the given
99      * {@link AlertEntry}
100      *
101      * @param alertEntry that will be considered clicked when onClick is called.
102      */
getClickHandler(AlertEntry alertEntry)103     public View.OnClickListener getClickHandler(AlertEntry alertEntry) {
104         return v -> {
105             Notification notification = alertEntry.getNotification();
106             final PendingIntent intent = notification.contentIntent != null
107                     ? notification.contentIntent
108                     : notification.fullScreenIntent;
109             if (intent == null) {
110                 return;
111             }
112 
113             int result = ActivityManager.START_ABORTED;
114             try {
115                 result = intent.sendAndReturnResult(/* context= */ null, /* code= */ 0,
116                         /* intent= */ null, /* onFinished= */ null,
117                         /* handler= */ null, /* requiredPermissions= */ null,
118                         /* options= */ null);
119             } catch (PendingIntent.CanceledException e) {
120                 // Do not take down the app over this
121                 Log.w(TAG, "Sending contentIntent failed: " + e);
122             }
123             NotificationVisibility notificationVisibility = NotificationVisibility.obtain(
124                     alertEntry.getKey(),
125                     /* rank= */ -1, /* count= */ -1, /* visible= */ true);
126             try {
127                 mBarService.onNotificationClick(alertEntry.getKey(),
128                         notificationVisibility);
129                 if (shouldAutoCancel(alertEntry)) {
130                     clearNotification(alertEntry);
131                 }
132             } catch (RemoteException ex) {
133                 Log.e(TAG, "Remote exception in getClickHandler", ex);
134             }
135             handleNotificationClicked(result, alertEntry);
136         };
137 
138     }
139 
140     /**
141      * Returns a {@link View.OnClickListener} that should be used for the
142      * {@link android.app.Notification.Action} contained in the {@link AlertEntry}
143      *
144      * @param alertEntry that contains the clicked action.
145      * @param index the index of the action clicked.
146      */
getActionClickHandler(AlertEntry alertEntry, int index)147     public View.OnClickListener getActionClickHandler(AlertEntry alertEntry, int index) {
148         return v -> {
149             Notification notification = alertEntry.getNotification();
150             Notification.Action action = notification.actions[index];
151             NotificationVisibility notificationVisibility = NotificationVisibility.obtain(
152                     alertEntry.getKey(),
153                     /* rank= */ -1, /* count= */ -1, /* visible= */ true);
154             boolean canceledExceptionThrown = false;
155             int semanticAction = action.getSemanticAction();
156             if (CarAssistUtils.isCarCompatibleMessagingNotification(
157                     alertEntry.getStatusBarNotification())) {
158                 if (semanticAction == Notification.Action.SEMANTIC_ACTION_REPLY) {
159                     Context context = v.getContext().getApplicationContext();
160                     Intent resultIntent = addCannedReplyMessage(action, context);
161                     int result = sendPendingIntent(action.actionIntent, context, resultIntent);
162                     if (result == ActivityManager.START_SUCCESS) {
163                         showToast(context, R.string.toast_message_sent_success);
164                     } else if (result == ActivityManager.START_ABORTED) {
165                         canceledExceptionThrown = true;
166                     }
167                 }
168             } else {
169                 int result = sendPendingIntent(action.actionIntent, /* context= */ null,
170                         /* resultIntent= */ null);
171                 if (result == ActivityManager.START_ABORTED) {
172                     canceledExceptionThrown = true;
173                 }
174                 handleNotificationClicked(result, alertEntry);
175             }
176             if (!canceledExceptionThrown) {
177                 try {
178                     mBarService.onNotificationActionClick(
179                             alertEntry.getKey(),
180                             index,
181                             action,
182                             notificationVisibility,
183                             /* generatedByAssistant= */ false);
184                 } catch (RemoteException e) {
185                     Log.e(TAG, "Remote exception in getActionClickHandler", e);
186                 }
187             }
188         };
189     }
190 
191     /**
192      * Returns a {@link View.OnClickListener} that should be used for the
193      * {@param messageNotification}'s {@param playButton}. Once the message is read aloud, the
194      * pending intent should be returned to the messaging app, so it can mark it as read.
195      */
196     public View.OnClickListener getPlayClickHandler(AlertEntry messageNotification) {
197         return view -> {
198             if (!CarAssistUtils.isCarCompatibleMessagingNotification(
199                     messageNotification.getStatusBarNotification())) {
200                 return;
201             }
202             Context context = view.getContext().getApplicationContext();
203             if (mCarAssistUtils == null) {
204                 mCarAssistUtils = new CarAssistUtils(context);
205             }
206             CarAssistUtils.ActionRequestCallback requestCallback = resultState -> {
207                 if (CarAssistUtils.ActionRequestCallback.RESULT_FAILED.equals(resultState)) {
208                     showToast(context, R.string.assist_action_failed_toast);
209                     Log.e(TAG, "Assistant failed to read aloud the message");
210                 }
211                 // Don't trigger mCallback so the shade remains open.
212             };
213             mCarAssistUtils.requestAssistantVoiceAction(
214                     messageNotification.getStatusBarNotification(),
215                     CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION,
216                     requestCallback);
217         };
218     }
219 
220     /**
221      * Returns a {@link View.OnClickListener} that should be used for the
222      * {@param messageNotification}'s {@param muteButton}.
223      */
224     public View.OnClickListener getMuteClickHandler(
225             Button muteButton, AlertEntry messageNotification) {
226         return v -> {
227             if (mNotificationDataManager != null) {
228                 mNotificationDataManager.toggleMute(messageNotification);
229                 Context context = v.getContext().getApplicationContext();
230                 muteButton.setText(
231                         (mNotificationDataManager.isMessageNotificationMuted(messageNotification))
232                                 ? context.getString(R.string.action_unmute_long)
233                                 : context.getString(R.string.action_mute_long));
234                 // Don't trigger mCallback so the shade remains open.
235             } else {
236                 Log.d(TAG, "Could not set mute click handler as NotificationDataManager is null");
237             }
238         };
239     }
240 
241     /**
242      * Registers a new {@link OnNotificationClickListener} to the list of click event listeners.
243      */
244     public void registerClickListener(OnNotificationClickListener clickListener) {
245         if (clickListener != null && !mClickListeners.contains(clickListener)) {
246             mClickListeners.add(clickListener);
247         }
248     }
249 
250     /**
251      * Unregisters a {@link OnNotificationClickListener} from the list of click event listeners.
252      */
253     public void unregisterClickListener(OnNotificationClickListener clickListener) {
254         mClickListeners.remove(clickListener);
255     }
256 
257     /**
258      * Clears all notifications.
259      */
260     public void clearAllNotifications() {
261         try {
262             mBarService.onClearAllNotifications(ActivityManager.getCurrentUser());
263         } catch (RemoteException e) {
264             Log.e(TAG, "clearAllNotifications: ", e);
265         }
266     }
267 
268     /**
269      * Clears the notifications provided.
270      */
271     public void clearNotifications(List<NotificationGroup> notificationsToClear) {
272         notificationsToClear.forEach(notificationGroup -> {
273             if (notificationGroup.isGroup()) {
274                 AlertEntry summaryNotification = notificationGroup.getGroupSummaryNotification();
275                 clearNotification(summaryNotification);
276             }
277             notificationGroup.getChildNotifications()
278                     .forEach(alertEntry -> clearNotification(alertEntry));
279         });
280     }
281 
282     /**
283      * Collapses the notification shade panel.
284      */
285     public void collapsePanel() {
286         try {
287             mBarService.collapsePanels();
288         } catch (RemoteException e) {
289             Log.e(TAG, "collapsePanel: ", e);
290         }
291     }
292 
293     /**
294      * Invokes all onNotificationClicked handlers registered in {@link OnNotificationClickListener}s
295      * array.
296      */
297     private void handleNotificationClicked(int launceResult, AlertEntry alertEntry) {
298         mClickListeners.forEach(
299                 listener -> listener.onNotificationClicked(launceResult, alertEntry));
300     }
301 
302     private void clearNotification(AlertEntry alertEntry) {
303         try {
304             // rank and count is used for logging and is not need at this time thus -1
305             NotificationVisibility notificationVisibility = NotificationVisibility.obtain(
306                     alertEntry.getKey(),
307                     /* rank= */ -1,
308                     /* count= */ -1,
309                     /* visible= */ true);
310 
311             mBarService.onNotificationClear(
312                     alertEntry.getStatusBarNotification().getPackageName(),
313                     alertEntry.getStatusBarNotification().getTag(),
314                     alertEntry.getStatusBarNotification().getId(),
315                     alertEntry.getStatusBarNotification().getUser().getIdentifier(),
316                     alertEntry.getStatusBarNotification().getKey(),
317                     NotificationStats.DISMISSAL_SHADE,
318                     NotificationStats.DISMISS_SENTIMENT_NEUTRAL,
319                     notificationVisibility);
320         } catch (RemoteException e) {
321             Log.e(TAG, "clearNotifications: ", e);
322         }
323     }
324 
325     private int sendPendingIntent(PendingIntent pendingIntent, Context context,
326             Intent resultIntent) {
327         try {
328             return pendingIntent.sendAndReturnResult(/* context= */ context, /* code= */ 0,
329                     /* intent= */ resultIntent, /* onFinished= */null,
330                     /* handler= */ null, /* requiredPermissions= */ null,
331                     /* options= */ null);
332         } catch (PendingIntent.CanceledException e) {
333             // Do not take down the app over this
334             Log.w(TAG, "Sending contentIntent failed: " + e);
335             return ActivityManager.START_ABORTED;
336         }
337     }
338 
339     /** Adds the canned reply sms message to the {@link Notification.Action}'s RemoteInput. **/
340     @Nullable
341     private Intent addCannedReplyMessage(Notification.Action action, Context context) {
342         RemoteInput remoteInput = action.getRemoteInputs()[0];
343         if (remoteInput == null) {
344             Log.w("TAG", "Cannot add canned reply message to action with no RemoteInput.");
345             return null;
346         }
347         Bundle messageDataBundle = new Bundle();
348         messageDataBundle.putCharSequence(remoteInput.getResultKey(),
349                 context.getString(R.string.canned_reply_message));
350         Intent resultIntent = new Intent();
351         RemoteInput.addResultsToIntent(
352                 new RemoteInput[]{remoteInput}, resultIntent, messageDataBundle);
353         return resultIntent;
354     }
355 
356     private void showToast(Context context, int resourceId) {
357         Toast toast = Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG);
358         // This flag is needed for the Toast to show up on the active user's screen since
359         // Notifications is part of SystemUI. SystemUI is owned by a system process, which runs in
360         // the background, so without this, the toast will never appear in the foreground.
361         toast.getWindowParams().privateFlags |=
362                 WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
363         toast.show();
364     }
365 
366     private boolean shouldAutoCancel(AlertEntry alertEntry) {
367         int flags = alertEntry.getNotification().flags;
368         if ((flags & Notification.FLAG_AUTO_CANCEL) != Notification.FLAG_AUTO_CANCEL) {
369             return false;
370         }
371         if ((flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
372             return false;
373         }
374         return true;
375     }
376 
377 }
378