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