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