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 android.telecom.Call.Details.PROPERTY_HIGH_DEF_AUDIO; 20 import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL; 21 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST; 22 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL; 23 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL; 24 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL; 25 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST; 26 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL; 27 28 import android.Manifest; 29 import android.app.ActivityManager; 30 import android.app.Notification; 31 import android.app.Notification.Builder; 32 import android.app.NotificationManager; 33 import android.app.PendingIntent; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.content.res.Resources; 37 import android.graphics.Bitmap; 38 import android.graphics.drawable.BitmapDrawable; 39 import android.graphics.drawable.Drawable; 40 import android.graphics.drawable.Icon; 41 import android.media.AudioAttributes; 42 import android.net.Uri; 43 import android.os.Build.VERSION; 44 import android.os.Build.VERSION_CODES; 45 import android.support.annotation.ColorRes; 46 import android.support.annotation.NonNull; 47 import android.support.annotation.Nullable; 48 import android.support.annotation.RequiresPermission; 49 import android.support.annotation.StringRes; 50 import android.support.annotation.VisibleForTesting; 51 import android.support.v4.os.BuildCompat; 52 import android.telecom.Call.Details; 53 import android.telecom.PhoneAccount; 54 import android.telecom.PhoneAccountHandle; 55 import android.telecom.TelecomManager; 56 import android.text.BidiFormatter; 57 import android.text.Spannable; 58 import android.text.SpannableString; 59 import android.text.TextDirectionHeuristics; 60 import android.text.TextUtils; 61 import android.text.style.ForegroundColorSpan; 62 import com.android.contacts.common.ContactsUtils; 63 import com.android.contacts.common.ContactsUtils.UserType; 64 import com.android.contacts.common.lettertiles.LetterTileDrawable; 65 import com.android.contacts.common.preference.ContactsPreferences; 66 import com.android.contacts.common.util.BitmapUtil; 67 import com.android.contacts.common.util.ContactDisplayUtils; 68 import com.android.dialer.common.LogUtil; 69 import com.android.dialer.enrichedcall.EnrichedCallComponent; 70 import com.android.dialer.enrichedcall.EnrichedCallManager; 71 import com.android.dialer.enrichedcall.Session; 72 import com.android.dialer.multimedia.MultimediaData; 73 import com.android.dialer.notification.NotificationChannelManager; 74 import com.android.dialer.notification.NotificationChannelManager.Channel; 75 import com.android.dialer.oem.MotorolaUtils; 76 import com.android.dialer.util.DrawableConverter; 77 import com.android.incallui.ContactInfoCache.ContactCacheEntry; 78 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 79 import com.android.incallui.InCallPresenter.InCallState; 80 import com.android.incallui.async.PausableExecutorImpl; 81 import com.android.incallui.call.CallList; 82 import com.android.incallui.call.DialerCall; 83 import com.android.incallui.call.DialerCallListener; 84 import com.android.incallui.ringtone.DialerRingtoneManager; 85 import com.android.incallui.ringtone.InCallTonePlayer; 86 import com.android.incallui.ringtone.ToneGeneratorFactory; 87 import com.android.incallui.videotech.utils.SessionModificationState; 88 import java.util.List; 89 import java.util.Locale; 90 import java.util.Objects; 91 92 /** This class adds Notifications to the status bar for the in-call experience. */ 93 public class StatusBarNotifier 94 implements InCallPresenter.InCallStateListener, EnrichedCallManager.StateChangedListener { 95 96 // Notification types 97 // Indicates that no notification is currently showing. 98 private static final int NOTIFICATION_NONE = 0; 99 // Notification for an active call. This is non-interruptive, but cannot be dismissed. 100 private static final int NOTIFICATION_IN_CALL = 1; 101 // Notification for incoming calls. This is interruptive and will show up as a HUN. 102 private static final int NOTIFICATION_INCOMING_CALL = 2; 103 // Notification for incoming calls in the case where there is already an active call. 104 // This is non-interruptive, but otherwise behaves the same as NOTIFICATION_INCOMING_CALL 105 private static final int NOTIFICATION_INCOMING_CALL_QUIET = 3; 106 107 private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0; 108 private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1; 109 110 private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000}; 111 112 private final Context mContext; 113 private final ContactInfoCache mContactInfoCache; 114 private final NotificationManager mNotificationManager; 115 private final DialerRingtoneManager mDialerRingtoneManager; 116 @Nullable private ContactsPreferences mContactsPreferences; 117 private int mCurrentNotification = NOTIFICATION_NONE; 118 private int mCallState = DialerCall.State.INVALID; 119 private int mSavedIcon = 0; 120 private String mSavedContent = null; 121 private Bitmap mSavedLargeIcon; 122 private String mSavedContentTitle; 123 private Uri mRingtone; 124 private StatusBarCallListener mStatusBarCallListener; 125 StatusBarNotifier(@onNull Context context, @NonNull ContactInfoCache contactInfoCache)126 public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) { 127 Objects.requireNonNull(context); 128 mContext = context; 129 mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); 130 mContactInfoCache = contactInfoCache; 131 mNotificationManager = context.getSystemService(NotificationManager.class); 132 mDialerRingtoneManager = 133 new DialerRingtoneManager( 134 new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()), 135 CallList.getInstance()); 136 mCurrentNotification = NOTIFICATION_NONE; 137 } 138 139 /** 140 * Should only be called from a irrecoverable state where it is necessary to dismiss all 141 * notifications. 142 */ clearAllCallNotifications(Context backupContext)143 static void clearAllCallNotifications(Context backupContext) { 144 LogUtil.i( 145 "StatusBarNotifier.clearAllCallNotifications", 146 "something terrible happened, clear all InCall notifications"); 147 148 NotificationManager notificationManager = 149 backupContext.getSystemService(NotificationManager.class); 150 notificationManager.cancel(R.id.notification_ongoing_call); 151 } 152 getWorkStringFromPersonalString(int resId)153 private static int getWorkStringFromPersonalString(int resId) { 154 if (resId == R.string.notification_ongoing_call) { 155 return R.string.notification_ongoing_work_call; 156 } else if (resId == R.string.notification_ongoing_call_wifi) { 157 return R.string.notification_ongoing_work_call_wifi; 158 } else if (resId == R.string.notification_incoming_call_wifi) { 159 return R.string.notification_incoming_work_call_wifi; 160 } else if (resId == R.string.notification_incoming_call) { 161 return R.string.notification_incoming_work_call; 162 } else { 163 return resId; 164 } 165 } 166 167 /** 168 * Returns PendingIntent for answering a phone call. This will typically be used from Notification 169 * context. 170 */ createNotificationPendingIntent(Context context, String action)171 private static PendingIntent createNotificationPendingIntent(Context context, String action) { 172 final Intent intent = new Intent(action, null, context, NotificationBroadcastReceiver.class); 173 return PendingIntent.getBroadcast(context, 0, intent, 0); 174 } 175 setColorized(@onNull Builder builder)176 private static void setColorized(@NonNull Builder builder) { 177 if (BuildCompat.isAtLeastO()) { 178 builder.setColorized(true); 179 } 180 } 181 182 /** Creates notifications according to the state we receive from {@link InCallPresenter}. */ 183 @Override 184 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) onStateChange(InCallState oldState, InCallState newState, CallList callList)185 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { 186 LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState); 187 updateNotification(callList); 188 } 189 190 @Override onEnrichedCallStateChanged()191 public void onEnrichedCallStateChanged() { 192 LogUtil.enterBlock("StatusBarNotifier.onEnrichedCallStateChanged"); 193 updateNotification(CallList.getInstance()); 194 } 195 196 /** 197 * Updates the phone app's status bar notification *and* launches the incoming call UI in response 198 * to a new incoming call. 199 * 200 * <p>If an incoming call is ringing (or call-waiting), the notification will also include a 201 * "fullScreenIntent" that will cause the InCallScreen to be launched, unless the current 202 * foreground activity is marked as "immersive". 203 * 204 * <p>(This is the mechanism that actually brings up the incoming call UI when we receive a "new 205 * ringing connection" event from the telephony layer.) 206 * 207 * <p>Also note that this method is safe to call even if the phone isn't actually ringing (or, 208 * more likely, if an incoming call *was* ringing briefly but then disconnected). In that case, 209 * we'll simply update or cancel the in-call notification based on the current phone state. 210 * 211 * @see #updateInCallNotification(CallList) 212 */ 213 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) updateNotification(CallList callList)214 public void updateNotification(CallList callList) { 215 updateInCallNotification(callList); 216 } 217 218 /** 219 * Take down the in-call notification. 220 * 221 * @see #updateInCallNotification(CallList) 222 */ cancelNotification()223 private void cancelNotification() { 224 if (mStatusBarCallListener != null) { 225 setStatusBarCallListener(null); 226 } 227 if (mCurrentNotification != NOTIFICATION_NONE) { 228 LogUtil.i("StatusBarNotifier.cancelNotification", "cancel"); 229 mNotificationManager.cancel(R.id.notification_ongoing_call); 230 } 231 mCurrentNotification = NOTIFICATION_NONE; 232 } 233 234 /** 235 * Helper method for updateInCallNotification() and updateNotification(): Update the phone app's 236 * status bar notification based on the current telephony state, or cancels the notification if 237 * the phone is totally idle. 238 */ 239 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) updateInCallNotification(CallList callList)240 private void updateInCallNotification(CallList callList) { 241 LogUtil.d("StatusBarNotifier.updateInCallNotification", ""); 242 243 final DialerCall call = getCallToShow(callList); 244 245 if (call != null) { 246 showNotification(callList, call); 247 } else { 248 cancelNotification(); 249 } 250 } 251 252 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) showNotification(final CallList callList, final DialerCall call)253 private void showNotification(final CallList callList, final DialerCall call) { 254 final boolean isIncoming = 255 (call.getState() == DialerCall.State.INCOMING 256 || call.getState() == DialerCall.State.CALL_WAITING); 257 setStatusBarCallListener(new StatusBarCallListener(call)); 258 259 // we make a call to the contact info cache to query for supplemental data to what the 260 // call provides. This includes the contact name and photo. 261 // This callback will always get called immediately and synchronously with whatever data 262 // it has available, and may make a subsequent call later (same thread) if it had to 263 // call into the contacts provider for more data. 264 mContactInfoCache.findInfo( 265 call, 266 isIncoming, 267 new ContactInfoCacheCallback() { 268 @Override 269 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) 270 public void onContactInfoComplete(String callId, ContactCacheEntry entry) { 271 DialerCall call = callList.getCallById(callId); 272 if (call != null) { 273 call.getLogState().contactLookupResult = entry.contactLookupResult; 274 buildAndSendNotification(callList, call, entry); 275 } 276 } 277 278 @Override 279 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) 280 public void onImageLoadComplete(String callId, ContactCacheEntry entry) { 281 DialerCall call = callList.getCallById(callId); 282 if (call != null) { 283 buildAndSendNotification(callList, call, entry); 284 } 285 } 286 }); 287 } 288 289 /** Sets up the main Ui for the notification */ 290 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) buildAndSendNotification( CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo)291 private void buildAndSendNotification( 292 CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) { 293 // This can get called to update an existing notification after contact information has come 294 // back. However, it can happen much later. Before we continue, we need to make sure that 295 // the call being passed in is still the one we want to show in the notification. 296 final DialerCall call = getCallToShow(callList); 297 if (call == null || !call.getId().equals(originalCall.getId())) { 298 return; 299 } 300 301 final int callState = call.getState(); 302 303 // Check if data has changed; if nothing is different, don't issue another notification. 304 final int iconResId = getIconToDisplay(call); 305 Bitmap largeIcon = getLargeIconToDisplay(mContext, contactInfo, call); 306 final String content = getContentString(call, contactInfo.userType); 307 final String contentTitle = getContentTitle(contactInfo, call); 308 309 final boolean isVideoUpgradeRequest = 310 call.getVideoTech().getSessionModificationState() 311 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; 312 final int notificationType; 313 if (callState == DialerCall.State.INCOMING 314 || callState == DialerCall.State.CALL_WAITING 315 || isVideoUpgradeRequest) { 316 boolean alreadyActive = 317 callList.getActiveOrBackgroundCall() != null 318 && InCallPresenter.getInstance().isShowingInCallUi(); 319 notificationType = 320 alreadyActive ? NOTIFICATION_INCOMING_CALL_QUIET : NOTIFICATION_INCOMING_CALL; 321 } else { 322 notificationType = NOTIFICATION_IN_CALL; 323 } 324 325 if (!checkForChangeAndSaveData( 326 iconResId, 327 content, 328 largeIcon, 329 contentTitle, 330 callState, 331 notificationType, 332 contactInfo.contactRingtoneUri)) { 333 return; 334 } 335 336 if (largeIcon != null) { 337 largeIcon = getRoundedIcon(largeIcon); 338 } 339 340 // This builder is used for the notification shown when the device is locked and the user 341 // has set their notification settings to 'hide sensitive content' 342 // {@see Notification.Builder#setPublicVersion}. 343 Notification.Builder publicBuilder = new Notification.Builder(mContext); 344 publicBuilder 345 .setSmallIcon(iconResId) 346 .setColor(mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme())) 347 // Hide work call state for the lock screen notification 348 .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT)); 349 setNotificationWhen(call, callState, publicBuilder); 350 351 // Builder for the notification shown when the device is unlocked or the user has set their 352 // notification settings to 'show all notification content'. 353 final Notification.Builder builder = getNotificationBuilder(); 354 builder.setPublicVersion(publicBuilder.build()); 355 356 // Set up the main intent to send the user to the in-call screen 357 builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */)); 358 359 // Set the intent as a full screen intent as well if a call is incoming 360 PhoneAccountHandle accountHandle = call.getAccountHandle(); 361 if (accountHandle == null) { 362 accountHandle = getAnyPhoneAccount(); 363 } 364 365 LogUtil.i("StatusBarNotifier.buildAndSendNotification", "notificationType=" + notificationType); 366 switch (notificationType) { 367 case NOTIFICATION_INCOMING_CALL: 368 NotificationChannelManager.applyChannel( 369 builder, mContext, Channel.INCOMING_CALL, accountHandle); 370 configureFullScreenIntent(builder, createLaunchPendingIntent(true /* isFullScreen */)); 371 // Set the notification category and bump the priority for incoming calls 372 builder.setCategory(Notification.CATEGORY_CALL); 373 // This will be ignored on O+ and handled by the channel 374 //noinspection deprecation 375 builder.setPriority(Notification.PRIORITY_MAX); 376 if (mCurrentNotification != NOTIFICATION_INCOMING_CALL) { 377 LogUtil.i( 378 "StatusBarNotifier.buildAndSendNotification", 379 "Canceling old notification so this one can be noisy"); 380 // Moving from a non-interuptive notification (or none) to a noisy one. Cancel the old 381 // notification (if there is one) so the fullScreenIntent or HUN will show 382 mNotificationManager.cancel(R.id.notification_ongoing_call); 383 } 384 break; 385 case NOTIFICATION_INCOMING_CALL_QUIET: 386 NotificationChannelManager.applyChannel( 387 builder, mContext, Channel.ONGOING_CALL, accountHandle); 388 break; 389 case NOTIFICATION_IN_CALL: 390 setColorized(publicBuilder); 391 setColorized(builder); 392 NotificationChannelManager.applyChannel( 393 builder, mContext, Channel.ONGOING_CALL, accountHandle); 394 break; 395 } 396 397 // Set the content 398 builder.setContentText(content); 399 builder.setSmallIcon(iconResId); 400 builder.setContentTitle(contentTitle); 401 builder.setLargeIcon(largeIcon); 402 builder.setColor( 403 mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme())); 404 405 if (isVideoUpgradeRequest) { 406 builder.setUsesChronometer(false); 407 addDismissUpgradeRequestAction(builder); 408 addAcceptUpgradeRequestAction(builder); 409 } else { 410 createIncomingCallNotification(call, callState, builder); 411 } 412 413 addPersonReference(builder, contactInfo, call); 414 415 // Fire off the notification 416 Notification notification = builder.build(); 417 418 if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) { 419 notification.flags |= Notification.FLAG_INSISTENT; 420 notification.sound = contactInfo.contactRingtoneUri; 421 AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder(); 422 audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC); 423 audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); 424 notification.audioAttributes = audioAttributes.build(); 425 if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) { 426 notification.vibrate = VIBRATE_PATTERN; 427 } 428 } 429 if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) { 430 LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone"); 431 mDialerRingtoneManager.playCallWaitingTone(); 432 } 433 434 LogUtil.i( 435 "StatusBarNotifier.buildAndSendNotification", 436 "displaying notification for " + notificationType); 437 438 try { 439 mNotificationManager.notify(R.id.notification_ongoing_call, notification); 440 } catch (RuntimeException e) { 441 // TODO(b/34744003): Move the memory stats into silent feedback PSD. 442 ActivityManager activityManager = mContext.getSystemService(ActivityManager.class); 443 ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo(); 444 activityManager.getMemoryInfo(memoryInfo); 445 throw new RuntimeException( 446 String.format( 447 Locale.US, 448 "Error displaying notification with photo type: %d (low memory? %b, availMem: %d)", 449 contactInfo.photoType, 450 memoryInfo.lowMemory, 451 memoryInfo.availMem), 452 e); 453 } 454 call.getLatencyReport().onNotificationShown(); 455 mCurrentNotification = notificationType; 456 } 457 458 @Nullable 459 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) getAnyPhoneAccount()460 private PhoneAccountHandle getAnyPhoneAccount() { 461 PhoneAccountHandle accountHandle; 462 TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class); 463 accountHandle = telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL); 464 if (accountHandle == null) { 465 List<PhoneAccountHandle> accountHandles = telecomManager.getCallCapablePhoneAccounts(); 466 if (!accountHandles.isEmpty()) { 467 accountHandle = accountHandles.get(0); 468 } 469 } 470 return accountHandle; 471 } 472 createIncomingCallNotification( DialerCall call, int state, Notification.Builder builder)473 private void createIncomingCallNotification( 474 DialerCall call, int state, Notification.Builder builder) { 475 setNotificationWhen(call, state, builder); 476 477 // Add hang up option for any active calls (active | onhold), outgoing calls (dialing). 478 if (state == DialerCall.State.ACTIVE 479 || state == DialerCall.State.ONHOLD 480 || DialerCall.State.isDialing(state)) { 481 addHangupAction(builder); 482 } else if (state == DialerCall.State.INCOMING || state == DialerCall.State.CALL_WAITING) { 483 addDismissAction(builder); 484 if (call.isVideoCall()) { 485 addVideoCallAction(builder); 486 } else { 487 addAnswerAction(builder); 488 } 489 } 490 } 491 492 /** 493 * Sets the notification's when section as needed. For active calls, this is explicitly set as the 494 * duration of the call. For all other states, the notification will automatically show the time 495 * at which the notification was created. 496 */ setNotificationWhen(DialerCall call, int state, Notification.Builder builder)497 private void setNotificationWhen(DialerCall call, int state, Notification.Builder builder) { 498 if (state == DialerCall.State.ACTIVE) { 499 builder.setUsesChronometer(true); 500 builder.setWhen(call.getConnectTimeMillis()); 501 } else { 502 builder.setUsesChronometer(false); 503 } 504 } 505 506 /** 507 * Checks the new notification data and compares it against any notification that we are already 508 * displaying. If the data is exactly the same, we return false so that we do not issue a new 509 * notification for the exact same data. 510 */ checkForChangeAndSaveData( int icon, String content, Bitmap largeIcon, String contentTitle, int state, int notificationType, Uri ringtone)511 private boolean checkForChangeAndSaveData( 512 int icon, 513 String content, 514 Bitmap largeIcon, 515 String contentTitle, 516 int state, 517 int notificationType, 518 Uri ringtone) { 519 520 // The two are different: 521 // if new title is not null, it should be different from saved version OR 522 // if new title is null, the saved version should not be null 523 final boolean contentTitleChanged = 524 (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) 525 || (contentTitle == null && mSavedContentTitle != null); 526 527 boolean largeIconChanged = 528 mSavedLargeIcon == null ? largeIcon != null : !mSavedLargeIcon.sameAs(largeIcon); 529 530 // any change means we are definitely updating 531 boolean retval = 532 (mSavedIcon != icon) 533 || !Objects.equals(mSavedContent, content) 534 || (mCallState != state) 535 || largeIconChanged 536 || contentTitleChanged 537 || !Objects.equals(mRingtone, ringtone); 538 539 // If we aren't showing a notification right now or the notification type is changing, 540 // definitely do an update. 541 if (mCurrentNotification != notificationType) { 542 if (mCurrentNotification == NOTIFICATION_NONE) { 543 LogUtil.d( 544 "StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time."); 545 } 546 retval = true; 547 } 548 549 mSavedIcon = icon; 550 mSavedContent = content; 551 mCallState = state; 552 mSavedLargeIcon = largeIcon; 553 mSavedContentTitle = contentTitle; 554 mRingtone = ringtone; 555 556 if (retval) { 557 LogUtil.d( 558 "StatusBarNotifier.checkForChangeAndSaveData", "data changed. Showing notification"); 559 } 560 561 return retval; 562 } 563 564 /** Returns the main string to use in the notification. */ 565 @VisibleForTesting 566 @Nullable getContentTitle(ContactCacheEntry contactInfo, DialerCall call)567 String getContentTitle(ContactCacheEntry contactInfo, DialerCall call) { 568 if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) { 569 return mContext.getResources().getString(R.string.conference_call_name); 570 } 571 572 String preferredName = 573 ContactDisplayUtils.getPreferredDisplayName( 574 contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences); 575 if (TextUtils.isEmpty(preferredName)) { 576 return TextUtils.isEmpty(contactInfo.number) 577 ? null 578 : BidiFormatter.getInstance() 579 .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); 580 } 581 return preferredName; 582 } 583 addPersonReference( Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call)584 private void addPersonReference( 585 Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call) { 586 // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. 587 // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid 588 // NotificationManager using it. 589 if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { 590 builder.addPerson(contactInfo.lookupUri.toString()); 591 } else if (!TextUtils.isEmpty(call.getNumber())) { 592 builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null).toString()); 593 } 594 } 595 596 /** Gets a large icon from the contact info object to display in the notification. */ getLargeIconToDisplay( Context context, ContactCacheEntry contactInfo, DialerCall call)597 private static Bitmap getLargeIconToDisplay( 598 Context context, ContactCacheEntry contactInfo, DialerCall call) { 599 Resources resources = context.getResources(); 600 Bitmap largeIcon = null; 601 if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { 602 largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); 603 } 604 if (contactInfo.photo == null) { 605 int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width); 606 int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height); 607 int contactType = LetterTileDrawable.TYPE_DEFAULT; 608 LetterTileDrawable lettertile = new LetterTileDrawable(resources); 609 610 // TODO: Deduplicate across Dialer. b/36195917 611 if (CallerInfoUtils.isVoiceMailNumber(context, call)) { 612 contactType = LetterTileDrawable.TYPE_VOICEMAIL; 613 } else if (contactInfo.isBusiness) { 614 contactType = LetterTileDrawable.TYPE_BUSINESS; 615 } else if (call.getNumberPresentation() == TelecomManager.PRESENTATION_RESTRICTED) { 616 contactType = LetterTileDrawable.TYPE_GENERIC_AVATAR; 617 } else if (call.isConferenceCall() 618 && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) { 619 contactType = LetterTileDrawable.TYPE_CONFERENCE; 620 } 621 lettertile.setCanonicalDialerLetterTileDetails( 622 contactInfo.namePrimary == null ? contactInfo.number : contactInfo.namePrimary, 623 contactInfo.lookupKey, 624 LetterTileDrawable.SHAPE_CIRCLE, 625 contactType); 626 largeIcon = lettertile.getBitmap(width, height); 627 } 628 629 if (call.isSpam()) { 630 Drawable drawable = resources.getDrawable(R.drawable.blocked_contact, context.getTheme()); 631 largeIcon = DrawableConverter.drawableToBitmap(drawable); 632 } 633 return largeIcon; 634 } 635 getRoundedIcon(Bitmap bitmap)636 private Bitmap getRoundedIcon(Bitmap bitmap) { 637 if (bitmap == null) { 638 return null; 639 } 640 final int height = 641 (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height); 642 final int width = 643 (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width); 644 return BitmapUtil.getRoundedBitmap(bitmap, width, height); 645 } 646 647 /** 648 * Returns the appropriate icon res Id to display based on the call for which we want to display 649 * information. 650 */ getIconToDisplay(DialerCall call)651 private int getIconToDisplay(DialerCall call) { 652 // Even if both lines are in use, we only show a single item in 653 // the expanded Notifications UI. It's labeled "Ongoing call" 654 // (or "On hold" if there's only one call, and it's on hold.) 655 // Also, we don't have room to display caller-id info from two 656 // different calls. So if both lines are in use, display info 657 // from the foreground call. And if there's a ringing call, 658 // display that regardless of the state of the other calls. 659 if (call.getState() == DialerCall.State.ONHOLD) { 660 return R.drawable.ic_phone_paused_white_24dp; 661 } else if (call.getVideoTech().getSessionModificationState() 662 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 663 return R.drawable.quantum_ic_videocam_white_24; 664 } else if (call.hasProperty(PROPERTY_HIGH_DEF_AUDIO) 665 && MotorolaUtils.shouldShowHdIconInNotification(mContext)) { 666 // Normally when a call is ongoing the status bar displays an icon of a phone with animated 667 // lines. This is a helpful hint for users so they know how to get back to the call. 668 // For Sprint HD calls, we replace this icon with an icon of a phone with a HD badge. 669 // This is a carrier requirement. 670 return R.drawable.ic_hd_call; 671 } 672 return R.anim.on_going_call; 673 } 674 675 /** Returns the message to use with the notification. */ getContentString(DialerCall call, @UserType long userType)676 private String getContentString(DialerCall call, @UserType long userType) { 677 boolean isIncomingOrWaiting = 678 call.getState() == DialerCall.State.INCOMING 679 || call.getState() == DialerCall.State.CALL_WAITING; 680 681 if (isIncomingOrWaiting 682 && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) { 683 684 if (!TextUtils.isEmpty(call.getChildNumber())) { 685 return mContext.getString(R.string.child_number, call.getChildNumber()); 686 } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) { 687 return call.getCallSubject(); 688 } 689 } 690 691 int resId = R.string.notification_ongoing_call; 692 if (call.hasProperty(Details.PROPERTY_WIFI)) { 693 resId = R.string.notification_ongoing_call_wifi; 694 } 695 696 if (isIncomingOrWaiting) { 697 EnrichedCallManager manager = EnrichedCallComponent.get(mContext).getEnrichedCallManager(); 698 Session session = null; 699 if (call.getNumber() != null) { 700 session = 701 manager.getSession( 702 call.getUniqueCallId(), 703 call.getNumber(), 704 manager.createIncomingCallComposerFilter()); 705 } 706 707 if (call.isSpam()) { 708 resId = R.string.notification_incoming_spam_call; 709 } else if (session != null) { 710 resId = getECIncomingCallText(session); 711 } else if (call.hasProperty(Details.PROPERTY_WIFI)) { 712 resId = R.string.notification_incoming_call_wifi; 713 } else { 714 resId = R.string.notification_incoming_call; 715 } 716 } else if (call.getState() == DialerCall.State.ONHOLD) { 717 resId = R.string.notification_on_hold; 718 } else if (DialerCall.State.isDialing(call.getState())) { 719 resId = R.string.notification_dialing; 720 } else if (call.getVideoTech().getSessionModificationState() 721 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 722 resId = R.string.notification_requesting_video_call; 723 } 724 725 // Is the call placed through work connection service. 726 boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL); 727 if (userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) { 728 resId = getWorkStringFromPersonalString(resId); 729 } 730 731 return mContext.getString(resId); 732 } 733 getECIncomingCallText(Session session)734 private int getECIncomingCallText(Session session) { 735 int resId; 736 MultimediaData data = session.getMultimediaData(); 737 boolean hasImage = data.hasImageData(); 738 boolean hasSubject = !TextUtils.isEmpty(data.getText()); 739 boolean hasMap = data.getLocation() != null; 740 if (data.isImportant()) { 741 if (hasMap) { 742 if (hasImage) { 743 if (hasSubject) { 744 resId = R.string.important_notification_incoming_call_with_photo_message_location; 745 } else { 746 resId = R.string.important_notification_incoming_call_with_photo_location; 747 } 748 } else if (hasSubject) { 749 resId = R.string.important_notification_incoming_call_with_message_location; 750 } else { 751 resId = R.string.important_notification_incoming_call_with_location; 752 } 753 } else if (hasImage) { 754 if (hasSubject) { 755 resId = R.string.important_notification_incoming_call_with_photo_message; 756 } else { 757 resId = R.string.important_notification_incoming_call_with_photo; 758 } 759 } else { 760 resId = R.string.important_notification_incoming_call_with_message; 761 } 762 if (mContext.getString(resId).length() > 50) { 763 resId = R.string.important_notification_incoming_call_attachments; 764 } 765 } else { 766 if (hasMap) { 767 if (hasImage) { 768 if (hasSubject) { 769 resId = R.string.notification_incoming_call_with_photo_message_location; 770 } else { 771 resId = R.string.notification_incoming_call_with_photo_location; 772 } 773 } else if (hasSubject) { 774 resId = R.string.notification_incoming_call_with_message_location; 775 } else { 776 resId = R.string.notification_incoming_call_with_location; 777 } 778 } else if (hasImage) { 779 if (hasSubject) { 780 resId = R.string.notification_incoming_call_with_photo_message; 781 } else { 782 resId = R.string.notification_incoming_call_with_photo; 783 } 784 } else { 785 resId = R.string.notification_incoming_call_with_message; 786 } 787 } 788 if (mContext.getString(resId).length() > 50) { 789 resId = R.string.notification_incoming_call_attachments; 790 } 791 return resId; 792 } 793 794 /** Gets the most relevant call to display in the notification. */ getCallToShow(CallList callList)795 private DialerCall getCallToShow(CallList callList) { 796 if (callList == null) { 797 return null; 798 } 799 DialerCall call = callList.getIncomingCall(); 800 if (call == null) { 801 call = callList.getOutgoingCall(); 802 } 803 if (call == null) { 804 call = callList.getVideoUpgradeRequestCall(); 805 } 806 if (call == null) { 807 call = callList.getActiveOrBackgroundCall(); 808 } 809 return call; 810 } 811 getActionText(@tringRes int stringRes, @ColorRes int colorRes)812 private Spannable getActionText(@StringRes int stringRes, @ColorRes int colorRes) { 813 Spannable spannable = new SpannableString(mContext.getText(stringRes)); 814 if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) { 815 // This will only work for cases where the Notification.Builder has a fullscreen intent set 816 // Notification.Builder that does not have a full screen intent will take the color of the 817 // app and the following leads to a no-op. 818 spannable.setSpan( 819 new ForegroundColorSpan(mContext.getColor(colorRes)), 0, spannable.length(), 0); 820 } 821 return spannable; 822 } 823 addAnswerAction(Notification.Builder builder)824 private void addAnswerAction(Notification.Builder builder) { 825 LogUtil.d( 826 "StatusBarNotifier.addAnswerAction", 827 "will show \"answer\" action in the incoming call Notification"); 828 PendingIntent answerVoicePendingIntent = 829 createNotificationPendingIntent(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL); 830 // We put animation resources in "anim" folder instead of "drawable", which causes Android 831 // Studio to complain. 832 // TODO: Move "anim" resources to "drawable" as recommended in AnimationDrawable doc? 833 //noinspection ResourceType 834 builder.addAction( 835 new Notification.Action.Builder( 836 Icon.createWithResource(mContext, R.anim.on_going_call), 837 getActionText( 838 R.string.notification_action_answer, R.color.notification_action_accept), 839 answerVoicePendingIntent) 840 .build()); 841 } 842 addDismissAction(Notification.Builder builder)843 private void addDismissAction(Notification.Builder builder) { 844 LogUtil.d( 845 "StatusBarNotifier.addDismissAction", 846 "will show \"decline\" action in the incoming call Notification"); 847 PendingIntent declinePendingIntent = 848 createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL); 849 builder.addAction( 850 new Notification.Action.Builder( 851 Icon.createWithResource(mContext, R.drawable.quantum_ic_close_white_24), 852 getActionText( 853 R.string.notification_action_dismiss, R.color.notification_action_dismiss), 854 declinePendingIntent) 855 .build()); 856 } 857 addHangupAction(Notification.Builder builder)858 private void addHangupAction(Notification.Builder builder) { 859 LogUtil.d( 860 "StatusBarNotifier.addHangupAction", 861 "will show \"hang-up\" action in the ongoing active call Notification"); 862 PendingIntent hangupPendingIntent = 863 createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL); 864 builder.addAction( 865 new Notification.Action.Builder( 866 Icon.createWithResource(mContext, R.drawable.ic_call_end_white_24dp), 867 mContext.getText(R.string.notification_action_end_call), 868 hangupPendingIntent) 869 .build()); 870 } 871 addVideoCallAction(Notification.Builder builder)872 private void addVideoCallAction(Notification.Builder builder) { 873 LogUtil.i( 874 "StatusBarNotifier.addVideoCallAction", 875 "will show \"video\" action in the incoming call Notification"); 876 PendingIntent answerVideoPendingIntent = 877 createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL); 878 builder.addAction( 879 new Notification.Action.Builder( 880 Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24), 881 getActionText( 882 R.string.notification_action_answer_video, 883 R.color.notification_action_answer_video), 884 answerVideoPendingIntent) 885 .build()); 886 } 887 addAcceptUpgradeRequestAction(Notification.Builder builder)888 private void addAcceptUpgradeRequestAction(Notification.Builder builder) { 889 LogUtil.i( 890 "StatusBarNotifier.addAcceptUpgradeRequestAction", 891 "will show \"accept upgrade\" action in the incoming call Notification"); 892 PendingIntent acceptVideoPendingIntent = 893 createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST); 894 builder.addAction( 895 new Notification.Action.Builder( 896 Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24), 897 getActionText( 898 R.string.notification_action_accept, R.color.notification_action_accept), 899 acceptVideoPendingIntent) 900 .build()); 901 } 902 addDismissUpgradeRequestAction(Notification.Builder builder)903 private void addDismissUpgradeRequestAction(Notification.Builder builder) { 904 LogUtil.i( 905 "StatusBarNotifier.addDismissUpgradeRequestAction", 906 "will show \"dismiss upgrade\" action in the incoming call Notification"); 907 PendingIntent declineVideoPendingIntent = 908 createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST); 909 builder.addAction( 910 new Notification.Action.Builder( 911 Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24), 912 getActionText( 913 R.string.notification_action_dismiss, R.color.notification_action_dismiss), 914 declineVideoPendingIntent) 915 .build()); 916 } 917 918 /** Adds fullscreen intent to the builder. */ configureFullScreenIntent(Notification.Builder builder, PendingIntent intent)919 private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent) { 920 // Ok, we actually want to launch the incoming call 921 // UI at this point (in addition to simply posting a notification 922 // to the status bar). Setting fullScreenIntent will cause 923 // the InCallScreen to be launched immediately *unless* the 924 // current foreground activity is marked as "immersive". 925 LogUtil.d("StatusBarNotifier.configureFullScreenIntent", "setting fullScreenIntent: " + intent); 926 builder.setFullScreenIntent(intent, true); 927 } 928 getNotificationBuilder()929 private Notification.Builder getNotificationBuilder() { 930 final Notification.Builder builder = new Notification.Builder(mContext); 931 builder.setOngoing(true); 932 builder.setOnlyAlertOnce(true); 933 // This will be ignored on O+ and handled by the channel 934 //noinspection deprecation 935 builder.setPriority(Notification.PRIORITY_HIGH); 936 937 return builder; 938 } 939 createLaunchPendingIntent(boolean isFullScreen)940 private PendingIntent createLaunchPendingIntent(boolean isFullScreen) { 941 Intent intent = 942 InCallActivity.getIntent( 943 mContext, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen); 944 945 int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN; 946 if (isFullScreen) { 947 // Use a unique request code so that the pending intent isn't clobbered by the 948 // non-full screen pending intent. 949 requestCode = PENDING_INTENT_REQUEST_CODE_FULL_SCREEN; 950 } 951 952 // PendingIntent that can be used to launch the InCallActivity. The 953 // system fires off this intent if the user pulls down the windowshade 954 // and clicks the notification's expanded view. It's also used to 955 // launch the InCallActivity immediately when when there's an incoming 956 // call (see the "fullScreenIntent" field below). 957 return PendingIntent.getActivity(mContext, requestCode, intent, 0); 958 } 959 setStatusBarCallListener(StatusBarCallListener listener)960 private void setStatusBarCallListener(StatusBarCallListener listener) { 961 if (mStatusBarCallListener != null) { 962 mStatusBarCallListener.cleanup(); 963 } 964 mStatusBarCallListener = listener; 965 } 966 967 private class StatusBarCallListener implements DialerCallListener { 968 969 private DialerCall mDialerCall; 970 StatusBarCallListener(DialerCall dialerCall)971 StatusBarCallListener(DialerCall dialerCall) { 972 mDialerCall = dialerCall; 973 mDialerCall.addListener(this); 974 } 975 cleanup()976 void cleanup() { 977 mDialerCall.removeListener(this); 978 } 979 980 @Override onDialerCallDisconnect()981 public void onDialerCallDisconnect() {} 982 983 @Override onDialerCallUpdate()984 public void onDialerCallUpdate() { 985 if (CallList.getInstance().getIncomingCall() == null) { 986 mDialerRingtoneManager.stopCallWaitingTone(); 987 } 988 } 989 990 @Override onDialerCallChildNumberChange()991 public void onDialerCallChildNumberChange() {} 992 993 @Override onDialerCallLastForwardedNumberChange()994 public void onDialerCallLastForwardedNumberChange() {} 995 996 @Override onDialerCallUpgradeToVideo()997 public void onDialerCallUpgradeToVideo() {} 998 999 @Override onWiFiToLteHandover()1000 public void onWiFiToLteHandover() {} 1001 1002 @Override onHandoverToWifiFailure()1003 public void onHandoverToWifiFailure() {} 1004 1005 @Override onInternationalCallOnWifi()1006 public void onInternationalCallOnWifi() {} 1007 1008 /** 1009 * Responds to changes in the session modification state for the call by dismissing the status 1010 * bar notification as required. 1011 */ 1012 @Override onDialerCallSessionModificationStateChange()1013 public void onDialerCallSessionModificationStateChange() { 1014 if (mDialerCall.getVideoTech().getSessionModificationState() 1015 == SessionModificationState.NO_REQUEST) { 1016 cleanup(); 1017 updateNotification(CallList.getInstance()); 1018 } 1019 } 1020 } 1021 } 1022