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.service.notification.StatusBarNotification;
30 import android.util.Log;
31 import android.view.View;
32 import android.view.WindowManager;
33 import android.widget.Button;
34 import android.widget.Toast;
35 
36 import com.android.car.assist.CarVoiceInteractionSession;
37 import com.android.car.assist.client.CarAssistUtils;
38 import com.android.internal.statusbar.IStatusBarService;
39 import com.android.internal.statusbar.NotificationVisibility;
40 
41 /**
42  * Factory that builds a {@link View.OnClickListener} to handle the logic of what to do when a
43  * notification is clicked. It also handles the interaction with the StatusBarService.
44  */
45 public class NotificationClickHandlerFactory {
46     private static final String TAG = "NotificationClickHandlerFactory";
47 
48     private final IStatusBarService mBarService;
49     private final Callback mCallback;
50     private CarAssistUtils mCarAssistUtils;
51     @Nullable private CarHeadsUpNotificationManager.Callback mHeadsUpManagerCallback;
52     @Nullable private NotificationDataManager mNotificationDataManager;
53 
NotificationClickHandlerFactory(IStatusBarService barService, @Nullable Callback callback)54     public NotificationClickHandlerFactory(IStatusBarService barService,
55             @Nullable Callback callback) {
56         mBarService = barService;
57         mCallback = callback != null ? callback : launchResult -> { };
58         mCarAssistUtils = null;
59     }
60 
61     /**
62      * Sets the {@link NotificationDataManager} which contains additional state information of the
63      * {@link StatusBarNotification}s.
64      */
setNotificationDataManager(NotificationDataManager manager)65     public void setNotificationDataManager(NotificationDataManager manager) {
66       mNotificationDataManager = manager;
67     }
68 
69     /**
70      * Returns the {@link NotificationDataManager} which contains additional state information of
71      * the {@link StatusBarNotification}s.
72      */
73     @Nullable
getNotificationDataManager()74     public NotificationDataManager getNotificationDataManager() {
75       return mNotificationDataManager;
76     }
77 
78     /**
79      * Returns a {@link View.OnClickListener} that should be used for the given
80      * {@link StatusBarNotification}
81      *
82      * @param statusBarNotification that will be considered clicked when onClick is called.
83      */
getClickHandler(StatusBarNotification statusBarNotification)84     public View.OnClickListener getClickHandler(StatusBarNotification statusBarNotification) {
85         return v -> {
86             Notification notification = statusBarNotification.getNotification();
87             final PendingIntent intent = notification.contentIntent != null
88                     ? notification.contentIntent
89                     : notification.fullScreenIntent;
90             if (intent == null) {
91                 return;
92             }
93             if (mHeadsUpManagerCallback != null) {
94                 mHeadsUpManagerCallback.clearHeadsUpNotification();
95             }
96             int result = ActivityManager.START_ABORTED;
97             try {
98                 result = intent.sendAndReturnResult(/* context= */ null, /* code= */ 0,
99                         /* intent= */ null, /* onFinished= */ null,
100                         /* handler= */ null, /* requiredPermissions= */ null,
101                         /* options= */ null);
102             } catch (PendingIntent.CanceledException e) {
103                 // Do not take down the app over this
104                 Log.w(TAG, "Sending contentIntent failed: " + e);
105             }
106             NotificationVisibility notificationVisibility = NotificationVisibility.obtain(
107                     statusBarNotification.getKey(),
108                     /* rank= */ -1, /* count= */ -1, /* visible= */ true);
109             try {
110                 mBarService.onNotificationClick(statusBarNotification.getKey(),
111                         notificationVisibility);
112                 if (shouldAutoCancel(statusBarNotification)) {
113                     mBarService.onNotificationClear(
114                             statusBarNotification.getPackageName(),
115                             statusBarNotification.getTag(),
116                             statusBarNotification.getId(),
117                             statusBarNotification.getUser().getIdentifier(),
118                             statusBarNotification.getKey(),
119                             NotificationStats.DISMISSAL_SHADE,
120                             NotificationStats.DISMISS_SENTIMENT_NEUTRAL,
121                             notificationVisibility);
122                 }
123             } catch (RemoteException ex) {
124                 Log.e(TAG, "Remote exception in getClickHandler", ex);
125             }
126             mCallback.onNotificationClicked(result);
127         };
128 
129     }
130 
setHeadsUpNotificationCallBack( @ullable CarHeadsUpNotificationManager.Callback callback)131     public void setHeadsUpNotificationCallBack(
132             @Nullable CarHeadsUpNotificationManager.Callback callback) {
133         mHeadsUpManagerCallback = callback;
134     }
135 
136     /**
137      * Returns a {@link View.OnClickListener} that should be used for the
138      * {@link android.app.Notification.Action} contained in the {@link StatusBarNotification}
139      *
140      * @param statusBarNotification that contains the clicked action.
141      * @param index the index of the action clicked
142      */
getActionClickHandler( StatusBarNotification statusBarNotification, int index)143     public View.OnClickListener getActionClickHandler(
144             StatusBarNotification statusBarNotification, int index) {
145         return v -> {
146             Notification notification = statusBarNotification.getNotification();
147             Notification.Action action = notification.actions[index];
148             NotificationVisibility notificationVisibility = NotificationVisibility.obtain(
149                     statusBarNotification.getKey(),
150                     /* rank= */ -1, /* count= */ -1, /* visible= */ true);
151             boolean canceledExceptionThrown = false;
152             int semanticAction = action.getSemanticAction();
153             if (CarAssistUtils.isCarCompatibleMessagingNotification(statusBarNotification)) {
154                 if (semanticAction == Notification.Action.SEMANTIC_ACTION_REPLY) {
155                     Context context = v.getContext().getApplicationContext();
156                     Intent resultIntent = addCannedReplyMessage(action, context);
157                     int result = sendPendingIntent(action.actionIntent, context, resultIntent);
158                     if (result == ActivityManager.START_SUCCESS) {
159                         showToast(context, R.string.toast_message_sent_success);
160                     } else if (result == ActivityManager.START_ABORTED) {
161                         canceledExceptionThrown = true;
162                     }
163                 }
164             } else {
165                 int result = sendPendingIntent(action.actionIntent, /* context= */ null,
166                         /* resultIntent= */ null);
167                 if (result == ActivityManager.START_ABORTED) {
168                     canceledExceptionThrown = true;
169                 }
170                 mCallback.onNotificationClicked(result);
171             }
172             if (!canceledExceptionThrown) {
173                 try {
174                     mBarService.onNotificationActionClick(
175                             statusBarNotification.getKey(),
176                             index,
177                             action,
178                             notificationVisibility,
179                             /* generatedByAssistant= */ false);
180                 } catch (RemoteException e) {
181                     Log.e(TAG, "Remote exception in getActionClickHandler", e);
182                 }
183             }
184         };
185     }
186 
187     /**
188      * Returns a {@link View.OnClickListener} that should be used for the
189      * {@param messageNotification}'s {@param playButton}. Once the message is read aloud, the
190      * pending intent should be returned to the messaging app, so it can mark it as read.
191      */
192     public View.OnClickListener getPlayClickHandler(StatusBarNotification messageNotification) {
193         return view -> {
194             if (!CarAssistUtils.isCarCompatibleMessagingNotification(messageNotification)) {
195                 return;
196             }
197             Context context = view.getContext().getApplicationContext();
198             if (mCarAssistUtils == null) {
199                 mCarAssistUtils = new CarAssistUtils(context);
200             }
201             CarAssistUtils.ActionRequestCallback requestCallback = resultState -> {
202                 if (CarAssistUtils.ActionRequestCallback.RESULT_FAILED.equals(resultState)) {
203                     showToast(context, R.string.assist_action_failed_toast);
204                     Log.e(TAG, "Assistant failed to read aloud the message");
205                 }
206                 // Don't trigger mCallback so the shade remains open.
207             };
208             mCarAssistUtils.requestAssistantVoiceAction(messageNotification,
209                     CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION,
210                     requestCallback);
211         };
212     }
213 
214     /**
215      * Returns a {@link View.OnClickListener} that should be used for the
216      * {@param messageNotification}'s {@param muteButton}.
217      */
218     public View.OnClickListener getMuteClickHandler(
219             Button muteButton, StatusBarNotification messageNotification) {
220         return v -> {
221             if (mNotificationDataManager != null) {
222                 mNotificationDataManager.toggleMute(messageNotification);
223                 Context context = v.getContext().getApplicationContext();
224                 muteButton.setText(
225                         (mNotificationDataManager.isMessageNotificationMuted(messageNotification))
226                                 ? context.getString(R.string.action_unmute_long)
227                                 : context.getString(R.string.action_mute_long));
228                 // Don't trigger mCallback so the shade remains open.
229             } else {
230               Log.d(TAG, "Could not set mute click handler as NotificationDataManager is null");
231             }
232         };
233     }
234 
235     private int sendPendingIntent(PendingIntent pendingIntent, Context context,
236             Intent resultIntent) {
237         try {
238             return pendingIntent.sendAndReturnResult(/* context= */ context, /* code= */ 0,
239                     /* intent= */ resultIntent, /* onFinished= */null,
240                     /* handler= */ null, /* requiredPermissions= */ null,
241                     /* options= */ null);
242         } catch (PendingIntent.CanceledException e) {
243             // Do not take down the app over this
244             Log.w(TAG, "Sending contentIntent failed: " + e);
245             return ActivityManager.START_ABORTED;
246         }
247     }
248 
249     /** Adds the canned reply sms message to the {@link Notification.Action}'s RemoteInput. **/
250     @Nullable
251     private Intent addCannedReplyMessage(Notification.Action action, Context context) {
252         RemoteInput remoteInput = action.getRemoteInputs()[0];
253         if (remoteInput == null) {
254             Log.w("TAG", "Cannot add canned reply message to action with no RemoteInput.");
255             return null;
256         }
257         Bundle messageDataBundle = new Bundle();
258         messageDataBundle.putCharSequence(remoteInput.getResultKey(),
259                 context.getString(R.string.canned_reply_message));
260         Intent resultIntent = new Intent();
261         RemoteInput.addResultsToIntent(
262                 new RemoteInput[]{remoteInput}, resultIntent, messageDataBundle);
263         return resultIntent;
264     }
265 
266     private void showToast(Context context, int resourceId) {
267         Toast toast = Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG);
268         // This flag is needed for the Toast to show up on the active user's screen since
269         // Notifications is part of SystemUI. SystemUI is owned by a system process, which runs in
270         // the background, so without this, the toast will never appear in the foreground.
271         toast.getWindowParams().privateFlags |=
272                 WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
273         toast.show();
274     }
275 
276     private boolean shouldAutoCancel(StatusBarNotification sbn) {
277         int flags = sbn.getNotification().flags;
278         if ((flags & Notification.FLAG_AUTO_CANCEL) != Notification.FLAG_AUTO_CANCEL) {
279             return false;
280         }
281         if ((flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
282             return false;
283         }
284         return true;
285     }
286 
287     public void clearAllNotifications() {
288         try {
289             mBarService.onClearAllNotifications(ActivityManager.getCurrentUser());
290         } catch (RemoteException e) {
291             Log.e(TAG, "clearAllNotifications: ", e);
292         }
293     }
294 
295     /**
296      * Callback that will be issued after a notification is clicked
297      */
298     public interface Callback {
299 
300         /**
301          * A notification was clicked and an onClickListener was fired.
302          *
303          * @param launchResult For non-Assistant actions, returned from
304          *        {@link PendingIntent#sendAndReturnResult}; for Assistant actions,
305          *        returns {@link ActivityManager#START_SUCCESS} on success;
306          *        {@link ActivityManager#START_ABORTED} otherwise.
307          */
308         void onNotificationClicked(int launchResult);
309     }
310 }
311