1 /* 2 * Copyright (C) 2013 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.incallui; 18 19 import static com.android.contacts.common.compat.CallSdkCompat.Details.PROPERTY_ENTERPRISE_CALL; 20 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST; 21 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL; 22 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL; 23 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL; 24 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST; 25 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL; 26 27 import com.google.common.base.Preconditions; 28 29 import android.app.Notification; 30 import android.app.NotificationManager; 31 import android.app.PendingIntent; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.graphics.Bitmap; 35 import android.graphics.BitmapFactory; 36 import android.graphics.drawable.BitmapDrawable; 37 import android.media.AudioAttributes; 38 import android.net.Uri; 39 import android.provider.ContactsContract.Contacts; 40 import android.support.annotation.Nullable; 41 import android.telecom.Call.Details; 42 import android.telecom.PhoneAccount; 43 import android.telecom.TelecomManager; 44 import android.text.BidiFormatter; 45 import android.text.TextDirectionHeuristics; 46 import android.text.TextUtils; 47 48 import com.android.contacts.common.ContactsUtils; 49 import com.android.contacts.common.ContactsUtils.UserType; 50 import com.android.contacts.common.preference.ContactsPreferences; 51 import com.android.contacts.common.testing.NeededForTesting; 52 import com.android.contacts.common.util.BitmapUtil; 53 import com.android.contacts.common.util.ContactDisplayUtils; 54 import com.android.dialer.R; 55 import com.android.incallui.ContactInfoCache.ContactCacheEntry; 56 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 57 import com.android.incallui.InCallPresenter.InCallState; 58 import com.android.incallui.async.PausableExecutorImpl; 59 import com.android.incallui.ringtone.DialerRingtoneManager; 60 import com.android.incallui.ringtone.InCallTonePlayer; 61 import com.android.incallui.ringtone.ToneGeneratorFactory; 62 63 import java.util.Objects; 64 65 /** 66 * This class adds Notifications to the status bar for the in-call experience. 67 */ 68 public class StatusBarNotifier implements InCallPresenter.InCallStateListener, 69 CallList.CallUpdateListener { 70 71 // Notification types 72 // Indicates that no notification is currently showing. 73 private static final int NOTIFICATION_NONE = 0; 74 // Notification for an active call. This is non-interruptive, but cannot be dismissed. 75 private static final int NOTIFICATION_IN_CALL = 1; 76 // Notification for incoming calls. This is interruptive and will show up as a HUN. 77 private static final int NOTIFICATION_INCOMING_CALL = 2; 78 79 private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000}; 80 81 private final Context mContext; 82 @Nullable private ContactsPreferences mContactsPreferences; 83 private final ContactInfoCache mContactInfoCache; 84 private final NotificationManager mNotificationManager; 85 private final DialerRingtoneManager mDialerRingtoneManager; 86 private int mCurrentNotification = NOTIFICATION_NONE; 87 private int mCallState = Call.State.INVALID; 88 private int mSavedIcon = 0; 89 private String mSavedContent = null; 90 private Bitmap mSavedLargeIcon; 91 private String mSavedContentTitle; 92 private String mCallId = null; 93 private InCallState mInCallState; 94 private Uri mRingtone; 95 96 public StatusBarNotifier(Context context, ContactInfoCache contactInfoCache) { 97 Preconditions.checkNotNull(context); 98 mContext = context; 99 mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); 100 mContactInfoCache = contactInfoCache; 101 mNotificationManager = 102 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 103 mDialerRingtoneManager = new DialerRingtoneManager( 104 new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()), 105 CallList.getInstance()); 106 mCurrentNotification = NOTIFICATION_NONE; 107 } 108 109 /** 110 * Creates notifications according to the state we receive from {@link InCallPresenter}. 111 */ 112 @Override 113 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { 114 Log.d(this, "onStateChange"); 115 mInCallState = newState; 116 updateNotification(newState, callList); 117 } 118 119 /** 120 * Updates the phone app's status bar notification *and* launches the 121 * incoming call UI in response to a new incoming call. 122 * 123 * If an incoming call is ringing (or call-waiting), the notification 124 * will also include a "fullScreenIntent" that will cause the 125 * InCallScreen to be launched, unless the current foreground activity 126 * is marked as "immersive". 127 * 128 * (This is the mechanism that actually brings up the incoming call UI 129 * when we receive a "new ringing connection" event from the telephony 130 * layer.) 131 * 132 * Also note that this method is safe to call even if the phone isn't 133 * actually ringing (or, more likely, if an incoming call *was* 134 * ringing briefly but then disconnected). In that case, we'll simply 135 * update or cancel the in-call notification based on the current 136 * phone state. 137 * 138 * @see #updateInCallNotification(InCallState,CallList) 139 */ 140 public void updateNotification(InCallState state, CallList callList) { 141 updateInCallNotification(state, callList); 142 } 143 144 /** 145 * Take down the in-call notification. 146 * @see #updateInCallNotification(InCallState,CallList) 147 */ 148 private void cancelNotification() { 149 if (!TextUtils.isEmpty(mCallId)) { 150 CallList.getInstance().removeCallUpdateListener(mCallId, this); 151 mCallId = null; 152 } 153 if (mCurrentNotification != NOTIFICATION_NONE) { 154 Log.d(this, "cancelInCall()..."); 155 mNotificationManager.cancel(mCurrentNotification); 156 } 157 mCurrentNotification = NOTIFICATION_NONE; 158 } 159 160 /** 161 * Should only be called from a irrecoverable state where it is necessary to dismiss all 162 * notifications. 163 */ 164 static void clearAllCallNotifications(Context backupContext) { 165 Log.i(StatusBarNotifier.class.getSimpleName(), 166 "Something terrible happened. Clear all InCall notifications"); 167 168 NotificationManager notificationManager = 169 (NotificationManager) backupContext.getSystemService(Context.NOTIFICATION_SERVICE); 170 notificationManager.cancel(NOTIFICATION_IN_CALL); 171 notificationManager.cancel(NOTIFICATION_INCOMING_CALL); 172 } 173 174 /** 175 * Helper method for updateInCallNotification() and 176 * updateNotification(): Update the phone app's 177 * status bar notification based on the current telephony state, or 178 * cancels the notification if the phone is totally idle. 179 */ 180 private void updateInCallNotification(final InCallState state, CallList callList) { 181 Log.d(this, "updateInCallNotification..."); 182 183 final Call call = getCallToShow(callList); 184 185 if (call != null) { 186 showNotification(call); 187 } else { 188 cancelNotification(); 189 } 190 } 191 192 private void showNotification(final Call call) { 193 final boolean isIncoming = (call.getState() == Call.State.INCOMING || 194 call.getState() == Call.State.CALL_WAITING); 195 if (!TextUtils.isEmpty(mCallId)) { 196 CallList.getInstance().removeCallUpdateListener(mCallId, this); 197 } 198 mCallId = call.getId(); 199 CallList.getInstance().addCallUpdateListener(call.getId(), this); 200 201 // we make a call to the contact info cache to query for supplemental data to what the 202 // call provides. This includes the contact name and photo. 203 // This callback will always get called immediately and synchronously with whatever data 204 // it has available, and may make a subsequent call later (same thread) if it had to 205 // call into the contacts provider for more data. 206 mContactInfoCache.findInfo(call, isIncoming, new ContactInfoCacheCallback() { 207 @Override 208 public void onContactInfoComplete(String callId, ContactCacheEntry entry) { 209 Call call = CallList.getInstance().getCallById(callId); 210 if (call != null) { 211 call.getLogState().contactLookupResult = entry.contactLookupResult; 212 buildAndSendNotification(call, entry); 213 } 214 } 215 216 @Override 217 public void onImageLoadComplete(String callId, ContactCacheEntry entry) { 218 Call call = CallList.getInstance().getCallById(callId); 219 if (call != null) { 220 buildAndSendNotification(call, entry); 221 } 222 } 223 224 @Override 225 public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {} 226 }); 227 } 228 229 /** 230 * Sets up the main Ui for the notification 231 */ 232 private void buildAndSendNotification(Call originalCall, ContactCacheEntry contactInfo) { 233 // This can get called to update an existing notification after contact information has come 234 // back. However, it can happen much later. Before we continue, we need to make sure that 235 // the call being passed in is still the one we want to show in the notification. 236 final Call call = getCallToShow(CallList.getInstance()); 237 if (call == null || !call.getId().equals(originalCall.getId())) { 238 return; 239 } 240 241 final int callState = call.getState(); 242 243 // Check if data has changed; if nothing is different, don't issue another notification. 244 final int iconResId = getIconToDisplay(call); 245 Bitmap largeIcon = getLargeIconToDisplay(contactInfo, call); 246 final String content = 247 getContentString(call, contactInfo.userType); 248 final String contentTitle = getContentTitle(contactInfo, call); 249 250 final boolean isVideoUpgradeRequest = call.getSessionModificationState() 251 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; 252 final int notificationType; 253 if (callState == Call.State.INCOMING || callState == Call.State.CALL_WAITING 254 || isVideoUpgradeRequest) { 255 notificationType = NOTIFICATION_INCOMING_CALL; 256 } else { 257 notificationType = NOTIFICATION_IN_CALL; 258 } 259 260 if (!checkForChangeAndSaveData(iconResId, content, largeIcon, contentTitle, callState, 261 notificationType, contactInfo.contactRingtoneUri)) { 262 return; 263 } 264 265 if (largeIcon != null) { 266 largeIcon = getRoundedIcon(largeIcon); 267 } 268 269 /* 270 * This builder is used for the notification shown when the device is locked and the user 271 * has set their notification settings to 'hide sensitive content' 272 * {@see Notification.Builder#setPublicVersion}. 273 */ 274 Notification.Builder publicBuilder = new Notification.Builder(mContext); 275 publicBuilder.setSmallIcon(iconResId) 276 .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) 277 // Hide work call state for the lock screen notification 278 .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT)); 279 setNotificationWhen(call, callState, publicBuilder); 280 281 /* 282 * Builder for the notification shown when the device is unlocked or the user has set their 283 * notification settings to 'show all notification content'. 284 */ 285 final Notification.Builder builder = getNotificationBuilder(); 286 builder.setPublicVersion(publicBuilder.build()); 287 288 // Set up the main intent to send the user to the in-call screen 289 final PendingIntent inCallPendingIntent = createLaunchPendingIntent(); 290 builder.setContentIntent(inCallPendingIntent); 291 292 // Set the intent as a full screen intent as well if a call is incoming 293 if (notificationType == NOTIFICATION_INCOMING_CALL 294 && !InCallPresenter.getInstance().isShowingInCallUi()) { 295 configureFullScreenIntent(builder, inCallPendingIntent, call); 296 // Set the notification category for incoming calls 297 builder.setCategory(Notification.CATEGORY_CALL); 298 } 299 300 // Set the content 301 builder.setContentText(content); 302 builder.setSmallIcon(iconResId); 303 builder.setContentTitle(contentTitle); 304 builder.setLargeIcon(largeIcon); 305 builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); 306 307 if (isVideoUpgradeRequest) { 308 builder.setUsesChronometer(false); 309 addDismissUpgradeRequestAction(builder); 310 addAcceptUpgradeRequestAction(builder); 311 } else { 312 createIncomingCallNotification(call, callState, builder); 313 } 314 315 addPersonReference(builder, contactInfo, call); 316 317 /* 318 * Fire off the notification 319 */ 320 Notification notification = builder.build(); 321 322 if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) { 323 notification.flags |= Notification.FLAG_INSISTENT; 324 notification.sound = contactInfo.contactRingtoneUri; 325 AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder(); 326 audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC); 327 audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); 328 notification.audioAttributes = audioAttributes.build(); 329 if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) { 330 notification.vibrate = VIBRATE_PATTERN; 331 } 332 } 333 if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) { 334 Log.v(this, "Playing call waiting tone"); 335 mDialerRingtoneManager.playCallWaitingTone(); 336 } 337 if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) { 338 Log.i(this, "Previous notification already showing - cancelling " 339 + mCurrentNotification); 340 mNotificationManager.cancel(mCurrentNotification); 341 } 342 Log.i(this, "Displaying notification for " + notificationType); 343 mNotificationManager.notify(notificationType, notification); 344 mCurrentNotification = notificationType; 345 } 346 347 private void createIncomingCallNotification( 348 Call call, int state, Notification.Builder builder) { 349 setNotificationWhen(call, state, builder); 350 351 // Add hang up option for any active calls (active | onhold), outgoing calls (dialing). 352 if (state == Call.State.ACTIVE || 353 state == Call.State.ONHOLD || 354 Call.State.isDialing(state)) { 355 addHangupAction(builder); 356 } else if (state == Call.State.INCOMING || state == Call.State.CALL_WAITING) { 357 addDismissAction(builder); 358 if (call.isVideoCall(mContext)) { 359 addVoiceAction(builder); 360 addVideoCallAction(builder); 361 } else { 362 addAnswerAction(builder); 363 } 364 } 365 } 366 367 /* 368 * Sets the notification's when section as needed. For active calls, this is explicitly set as 369 * the duration of the call. For all other states, the notification will automatically show the 370 * time at which the notification was created. 371 */ 372 private void setNotificationWhen(Call call, int state, Notification.Builder builder) { 373 if (state == Call.State.ACTIVE) { 374 builder.setUsesChronometer(true); 375 builder.setWhen(call.getConnectTimeMillis()); 376 } else { 377 builder.setUsesChronometer(false); 378 } 379 } 380 381 /** 382 * Checks the new notification data and compares it against any notification that we 383 * are already displaying. If the data is exactly the same, we return false so that 384 * we do not issue a new notification for the exact same data. 385 */ 386 private boolean checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon, 387 String contentTitle, int state, int notificationType, Uri ringtone) { 388 389 // The two are different: 390 // if new title is not null, it should be different from saved version OR 391 // if new title is null, the saved version should not be null 392 final boolean contentTitleChanged = 393 (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) || 394 (contentTitle == null && mSavedContentTitle != null); 395 396 // any change means we are definitely updating 397 boolean retval = (mSavedIcon != icon) || !Objects.equals(mSavedContent, content) 398 || (mCallState != state) || (mSavedLargeIcon != largeIcon) 399 || contentTitleChanged || !Objects.equals(mRingtone, ringtone); 400 401 // If we aren't showing a notification right now or the notification type is changing, 402 // definitely do an update. 403 if (mCurrentNotification != notificationType) { 404 if (mCurrentNotification == NOTIFICATION_NONE) { 405 Log.d(this, "Showing notification for first time."); 406 } 407 retval = true; 408 } 409 410 mSavedIcon = icon; 411 mSavedContent = content; 412 mCallState = state; 413 mSavedLargeIcon = largeIcon; 414 mSavedContentTitle = contentTitle; 415 mRingtone = ringtone; 416 417 if (retval) { 418 Log.d(this, "Data changed. Showing notification"); 419 } 420 421 return retval; 422 } 423 424 /** 425 * Returns the main string to use in the notification. 426 */ 427 @NeededForTesting 428 String getContentTitle(ContactCacheEntry contactInfo, Call call) { 429 if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) { 430 return mContext.getResources().getString(R.string.card_title_conf_call); 431 } 432 433 String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary, 434 contactInfo.nameAlternative, mContactsPreferences); 435 if (TextUtils.isEmpty(preferredName)) { 436 return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance() 437 .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); 438 } 439 return preferredName; 440 } 441 442 private void addPersonReference(Notification.Builder builder, ContactCacheEntry contactInfo, 443 Call call) { 444 // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. 445 // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid 446 // NotificationManager using it. 447 if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { 448 builder.addPerson(contactInfo.lookupUri.toString()); 449 } else if (!TextUtils.isEmpty(call.getNumber())) { 450 builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, 451 call.getNumber(), null).toString()); 452 } 453 } 454 455 /** 456 * Gets a large icon from the contact info object to display in the notification. 457 */ 458 private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, Call call) { 459 Bitmap largeIcon = null; 460 if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) { 461 largeIcon = BitmapFactory.decodeResource(mContext.getResources(), 462 R.drawable.img_conference); 463 } 464 if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { 465 largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); 466 } 467 return largeIcon; 468 } 469 470 private Bitmap getRoundedIcon(Bitmap bitmap) { 471 if (bitmap == null) { 472 return null; 473 } 474 final int height = (int) mContext.getResources().getDimension( 475 android.R.dimen.notification_large_icon_height); 476 final int width = (int) mContext.getResources().getDimension( 477 android.R.dimen.notification_large_icon_width); 478 return BitmapUtil.getRoundedBitmap(bitmap, width, height); 479 } 480 481 /** 482 * Returns the appropriate icon res Id to display based on the call for which 483 * we want to display information. 484 */ 485 private int getIconToDisplay(Call call) { 486 // Even if both lines are in use, we only show a single item in 487 // the expanded Notifications UI. It's labeled "Ongoing call" 488 // (or "On hold" if there's only one call, and it's on hold.) 489 // Also, we don't have room to display caller-id info from two 490 // different calls. So if both lines are in use, display info 491 // from the foreground call. And if there's a ringing call, 492 // display that regardless of the state of the other calls. 493 if (call.getState() == Call.State.ONHOLD) { 494 return R.drawable.ic_phone_paused_white_24dp; 495 } else if (call.getSessionModificationState() 496 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 497 return R.drawable.ic_videocam; 498 } 499 return R.drawable.ic_call_white_24dp; 500 } 501 502 /** 503 * Returns the message to use with the notification. 504 */ 505 private String getContentString(Call call, @UserType long userType) { 506 boolean isIncomingOrWaiting = call.getState() == Call.State.INCOMING || 507 call.getState() == Call.State.CALL_WAITING; 508 509 if (isIncomingOrWaiting && 510 call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) { 511 512 if (!TextUtils.isEmpty(call.getChildNumber())) { 513 return mContext.getString(R.string.child_number, call.getChildNumber()); 514 } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) { 515 return call.getCallSubject(); 516 } 517 } 518 519 int resId = R.string.notification_ongoing_call; 520 if (call.hasProperty(Details.PROPERTY_WIFI)) { 521 resId = R.string.notification_ongoing_call_wifi; 522 } 523 524 if (isIncomingOrWaiting) { 525 if (call.hasProperty(Details.PROPERTY_WIFI)) { 526 resId = R.string.notification_incoming_call_wifi; 527 } else { 528 resId = R.string.notification_incoming_call; 529 } 530 } else if (call.getState() == Call.State.ONHOLD) { 531 resId = R.string.notification_on_hold; 532 } else if (Call.State.isDialing(call.getState())) { 533 resId = R.string.notification_dialing; 534 } else if (call.getSessionModificationState() 535 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 536 resId = R.string.notification_requesting_video_call; 537 } 538 539 // Is the call placed through work connection service. 540 boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL); 541 if(userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) { 542 resId = getWorkStringFromPersonalString(resId); 543 } 544 545 return mContext.getString(resId); 546 } 547 548 private static int getWorkStringFromPersonalString(int resId) { 549 if (resId == R.string.notification_ongoing_call) { 550 return R.string.notification_ongoing_work_call; 551 } else if (resId == R.string.notification_ongoing_call_wifi) { 552 return R.string.notification_ongoing_work_call_wifi; 553 } else if (resId == R.string.notification_incoming_call_wifi) { 554 return R.string.notification_incoming_work_call_wifi; 555 } else if (resId == R.string.notification_incoming_call) { 556 return R.string.notification_incoming_work_call; 557 } else { 558 return resId; 559 } 560 } 561 562 /** 563 * Gets the most relevant call to display in the notification. 564 */ 565 private Call getCallToShow(CallList callList) { 566 if (callList == null) { 567 return null; 568 } 569 Call call = callList.getIncomingCall(); 570 if (call == null) { 571 call = callList.getOutgoingCall(); 572 } 573 if (call == null) { 574 call = callList.getVideoUpgradeRequestCall(); 575 } 576 if (call == null) { 577 call = callList.getActiveOrBackgroundCall(); 578 } 579 return call; 580 } 581 582 private void addAnswerAction(Notification.Builder builder) { 583 Log.d(this, "Will show \"answer\" action in the incoming call Notification"); 584 585 PendingIntent answerVoicePendingIntent = createNotificationPendingIntent( 586 mContext, ACTION_ANSWER_VOICE_INCOMING_CALL); 587 builder.addAction(R.drawable.ic_call_white_24dp, 588 mContext.getText(R.string.notification_action_answer), 589 answerVoicePendingIntent); 590 } 591 592 private void addDismissAction(Notification.Builder builder) { 593 Log.d(this, "Will show \"dismiss\" action in the incoming call Notification"); 594 595 PendingIntent declinePendingIntent = 596 createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL); 597 builder.addAction(R.drawable.ic_close_dk, 598 mContext.getText(R.string.notification_action_dismiss), 599 declinePendingIntent); 600 } 601 602 private void addHangupAction(Notification.Builder builder) { 603 Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification"); 604 605 PendingIntent hangupPendingIntent = 606 createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL); 607 builder.addAction(R.drawable.ic_call_end_white_24dp, 608 mContext.getText(R.string.notification_action_end_call), 609 hangupPendingIntent); 610 } 611 612 private void addVideoCallAction(Notification.Builder builder) { 613 Log.i(this, "Will show \"video\" action in the incoming call Notification"); 614 615 PendingIntent answerVideoPendingIntent = createNotificationPendingIntent( 616 mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL); 617 builder.addAction(R.drawable.ic_videocam, 618 mContext.getText(R.string.notification_action_answer_video), 619 answerVideoPendingIntent); 620 } 621 622 private void addVoiceAction(Notification.Builder builder) { 623 Log.d(this, "Will show \"voice\" action in the incoming call Notification"); 624 625 PendingIntent answerVoicePendingIntent = createNotificationPendingIntent( 626 mContext, ACTION_ANSWER_VOICE_INCOMING_CALL); 627 builder.addAction(R.drawable.ic_call_white_24dp, 628 mContext.getText(R.string.notification_action_answer_voice), 629 answerVoicePendingIntent); 630 } 631 632 private void addAcceptUpgradeRequestAction(Notification.Builder builder) { 633 Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification"); 634 635 PendingIntent acceptVideoPendingIntent = createNotificationPendingIntent( 636 mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST); 637 builder.addAction(0, mContext.getText(R.string.notification_action_accept), 638 acceptVideoPendingIntent); 639 } 640 641 private void addDismissUpgradeRequestAction(Notification.Builder builder) { 642 Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification"); 643 644 PendingIntent declineVideoPendingIntent = createNotificationPendingIntent( 645 mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST); 646 builder.addAction(0, mContext.getText(R.string.notification_action_dismiss), 647 declineVideoPendingIntent); 648 } 649 650 /** 651 * Adds fullscreen intent to the builder. 652 */ 653 private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent, 654 Call call) { 655 // Ok, we actually want to launch the incoming call 656 // UI at this point (in addition to simply posting a notification 657 // to the status bar). Setting fullScreenIntent will cause 658 // the InCallScreen to be launched immediately *unless* the 659 // current foreground activity is marked as "immersive". 660 Log.d(this, "- Setting fullScreenIntent: " + intent); 661 builder.setFullScreenIntent(intent, true); 662 663 // Ugly hack alert: 664 // 665 // The NotificationManager has the (undocumented) behavior 666 // that it will *ignore* the fullScreenIntent field if you 667 // post a new Notification that matches the ID of one that's 668 // already active. Unfortunately this is exactly what happens 669 // when you get an incoming call-waiting call: the 670 // "ongoing call" notification is already visible, so the 671 // InCallScreen won't get launched in this case! 672 // (The result: if you bail out of the in-call UI while on a 673 // call and then get a call-waiting call, the incoming call UI 674 // won't come up automatically.) 675 // 676 // The workaround is to just notice this exact case (this is a 677 // call-waiting call *and* the InCallScreen is not in the 678 // foreground) and manually cancel the in-call notification 679 // before (re)posting it. 680 // 681 // TODO: there should be a cleaner way of avoiding this 682 // problem (see discussion in bug 3184149.) 683 684 // If a call is onhold during an incoming call, the call actually comes in as 685 // INCOMING. For that case *and* traditional call-waiting, we want to 686 // cancel the notification. 687 boolean isCallWaiting = (call.getState() == Call.State.CALL_WAITING || 688 (call.getState() == Call.State.INCOMING && 689 CallList.getInstance().getBackgroundCall() != null)); 690 691 if (isCallWaiting) { 692 Log.i(this, "updateInCallNotification: call-waiting! force relaunch..."); 693 // Cancel the IN_CALL_NOTIFICATION immediately before 694 // (re)posting it; this seems to force the 695 // NotificationManager to launch the fullScreenIntent. 696 mNotificationManager.cancel(NOTIFICATION_IN_CALL); 697 } 698 } 699 700 private Notification.Builder getNotificationBuilder() { 701 final Notification.Builder builder = new Notification.Builder(mContext); 702 builder.setOngoing(true); 703 704 // Make the notification prioritized over the other normal notifications. 705 builder.setPriority(Notification.PRIORITY_HIGH); 706 707 return builder; 708 } 709 710 private PendingIntent createLaunchPendingIntent() { 711 712 final Intent intent = InCallPresenter.getInstance().getInCallIntent( 713 false /* showDialpad */, false /* newOutgoingCall */); 714 715 // PendingIntent that can be used to launch the InCallActivity. The 716 // system fires off this intent if the user pulls down the windowshade 717 // and clicks the notification's expanded view. It's also used to 718 // launch the InCallActivity immediately when when there's an incoming 719 // call (see the "fullScreenIntent" field below). 720 PendingIntent inCallPendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 721 722 return inCallPendingIntent; 723 } 724 725 /** 726 * Returns PendingIntent for answering a phone call. This will typically be used from 727 * Notification context. 728 */ 729 private static PendingIntent createNotificationPendingIntent(Context context, String action) { 730 final Intent intent = new Intent(action, null, 731 context, NotificationBroadcastReceiver.class); 732 return PendingIntent.getBroadcast(context, 0, intent, 0); 733 } 734 735 @Override 736 public void onCallChanged(Call call) { 737 if (CallList.getInstance().getIncomingCall() == null) { 738 mDialerRingtoneManager.stopCallWaitingTone(); 739 } 740 } 741 742 /** 743 * Responds to changes in the session modification state for the call by dismissing the 744 * status bar notification as required. 745 * 746 * @param sessionModificationState The new session modification state. 747 */ 748 @Override 749 public void onSessionModificationStateChange(int sessionModificationState) { 750 if (sessionModificationState == Call.SessionModificationState.NO_REQUEST) { 751 if (mCallId != null) { 752 CallList.getInstance().removeCallUpdateListener(mCallId, this); 753 } 754 755 updateNotification(mInCallState, CallList.getInstance()); 756 } 757 } 758 759 @Override 760 public void onLastForwardedNumberChange() { 761 // no-op 762 } 763 764 @Override 765 public void onChildNumberChange() { 766 // no-op 767 } 768 } 769