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