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 StatusBarNotifier(Context context, ContactInfoCache contactInfoCache)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 onStateChange(InCallState oldState, InCallState newState, CallList callList)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 */ updateNotification(InCallState state, CallList callList)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 */ cancelNotification()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 */ clearAllCallNotifications(Context backupContext)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 */ updateInCallNotification(final InCallState state, CallList callList)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 showNotification(final Call call)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 */ buildAndSendNotification(Call originalCall, ContactCacheEntry contactInfo)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 createIncomingCallNotification( Call call, int state, Notification.Builder builder)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 */ setNotificationWhen(Call call, int state, Notification.Builder builder)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 */ checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon, String contentTitle, int state, int notificationType, Uri ringtone)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 getContentTitle(ContactCacheEntry contactInfo, Call call)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 addPersonReference(Notification.Builder builder, ContactCacheEntry contactInfo, Call call)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 */ getLargeIconToDisplay(ContactCacheEntry contactInfo, Call call)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 getRoundedIcon(Bitmap bitmap)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 */ getIconToDisplay(Call call)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 */ getContentString(Call call, @UserType long userType)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 getWorkStringFromPersonalString(int resId)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 */ getCallToShow(CallList callList)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 addAnswerAction(Notification.Builder builder)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 addDismissAction(Notification.Builder builder)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 addHangupAction(Notification.Builder builder)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 addVideoCallAction(Notification.Builder builder)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 addVoiceAction(Notification.Builder builder)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 addAcceptUpgradeRequestAction(Notification.Builder builder)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 addDismissUpgradeRequestAction(Notification.Builder builder)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 */ configureFullScreenIntent(Notification.Builder builder, PendingIntent intent, Call call)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 getNotificationBuilder()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 createLaunchPendingIntent()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 */ createNotificationPendingIntent(Context context, String action)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 onCallChanged(Call call)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 onSessionModificationStateChange(int sessionModificationState)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 onLastForwardedNumberChange()760 public void onLastForwardedNumberChange() { 761 // no-op 762 } 763 764 @Override onChildNumberChange()765 public void onChildNumberChange() { 766 // no-op 767 } 768 } 769