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