/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.incallui; import static android.telecom.Call.Details.PROPERTY_HIGH_DEF_AUDIO; import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL; import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST; import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_SPEAKEASY_CALL; import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL; import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL; import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL; import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST; import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL; import static com.android.incallui.NotificationBroadcastReceiver.ACTION_TURN_OFF_SPEAKER; import static com.android.incallui.NotificationBroadcastReceiver.ACTION_TURN_ON_SPEAKER; import android.Manifest; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.media.AudioAttributes; import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Trace; import android.support.annotation.ColorRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresPermission; import android.support.annotation.StringRes; import android.support.annotation.VisibleForTesting; import android.support.v4.os.BuildCompat; import android.telecom.Call.Details; import android.telecom.CallAudioState; import android.telecom.PhoneAccount; import android.telecom.TelecomManager; import android.telecom.VideoProfile; import android.text.BidiFormatter; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.TextDirectionHeuristics; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import com.android.contacts.common.ContactsUtils; import com.android.contacts.common.ContactsUtils.UserType; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.configprovider.ConfigProviderComponent; import com.android.dialer.contactphoto.BitmapUtil; import com.android.dialer.contacts.ContactsComponent; import com.android.dialer.enrichedcall.EnrichedCallManager; import com.android.dialer.enrichedcall.Session; import com.android.dialer.lettertile.LetterTileDrawable; import com.android.dialer.lettertile.LetterTileDrawable.ContactType; import com.android.dialer.multimedia.MultimediaData; import com.android.dialer.notification.NotificationChannelId; import com.android.dialer.oem.MotorolaUtils; import com.android.dialer.theme.base.ThemeComponent; import com.android.dialer.util.DrawableConverter; import com.android.incallui.ContactInfoCache.ContactCacheEntry; import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; import com.android.incallui.InCallPresenter.InCallState; import com.android.incallui.async.PausableExecutorImpl; import com.android.incallui.audiomode.AudioModeProvider; import com.android.incallui.call.CallList; import com.android.incallui.call.DialerCall; import com.android.incallui.call.DialerCallListener; import com.android.incallui.call.TelecomAdapter; import com.android.incallui.call.state.DialerCallState; import com.android.incallui.ringtone.DialerRingtoneManager; import com.android.incallui.ringtone.InCallTonePlayer; import com.android.incallui.ringtone.ToneGeneratorFactory; import com.android.incallui.speakeasy.SpeakEasyComponent; import com.android.incallui.videotech.utils.SessionModificationState; import com.google.common.base.Optional; import java.util.Objects; /** This class adds Notifications to the status bar for the in-call experience. */ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, EnrichedCallManager.StateChangedListener, ContactInfoCacheCallback { private static final int NOTIFICATION_ID = 1; // Notification types // Indicates that no notification is currently showing. private static final int NOTIFICATION_NONE = 0; // Notification for an active call. This is non-interruptive, but cannot be dismissed. private static final int NOTIFICATION_IN_CALL = 1; // Notification for incoming calls. This is interruptive and will show up as a HUN. private static final int NOTIFICATION_INCOMING_CALL = 2; // Notification for incoming calls in the case where there is already an active call. // This is non-interruptive, but otherwise behaves the same as NOTIFICATION_INCOMING_CALL private static final int NOTIFICATION_INCOMING_CALL_QUIET = 3; private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000}; private final Context context; private final ContactInfoCache contactInfoCache; private final DialerRingtoneManager dialerRingtoneManager; private int currentNotification = NOTIFICATION_NONE; private int callState = DialerCallState.INVALID; private int videoState = VideoProfile.STATE_AUDIO_ONLY; private int savedIcon = 0; private String savedContent = null; private Bitmap savedLargeIcon; private String savedContentTitle; private CallAudioState savedCallAudioState; private Uri ringtone; private StatusBarCallListener statusBarCallListener; public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) { Trace.beginSection("StatusBarNotifier.Constructor"); this.context = Assert.isNotNull(context); this.contactInfoCache = contactInfoCache; dialerRingtoneManager = new DialerRingtoneManager( new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()), CallList.getInstance()); currentNotification = NOTIFICATION_NONE; Trace.endSection(); } /** * Should only be called from a irrecoverable state where it is necessary to dismiss all * notifications. */ static void clearAllCallNotifications() { LogUtil.e( "StatusBarNotifier.clearAllCallNotifications", "something terrible happened, clear all InCall notifications"); TelecomAdapter.getInstance().stopForegroundNotification(); } private static int getWorkStringFromPersonalString(int resId) { if (resId == R.string.notification_ongoing_call) { return R.string.notification_ongoing_work_call; } else if (resId == R.string.notification_incoming_call) { return R.string.notification_incoming_work_call; } else { return resId; } } /** * Returns PendingIntent for answering a phone call. This will typically be used from Notification * context. */ private static PendingIntent createNotificationPendingIntent(Context context, String action) { final Intent intent = new Intent(action, null, context, NotificationBroadcastReceiver.class); return PendingIntent.getBroadcast(context, 0, intent, 0); } /** Creates notifications according to the state we receive from {@link InCallPresenter}. */ @Override @RequiresPermission(Manifest.permission.READ_PHONE_STATE) public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState); updateNotification(); } @Override public void onEnrichedCallStateChanged() { LogUtil.enterBlock("StatusBarNotifier.onEnrichedCallStateChanged"); updateNotification(); } /** * Updates the phone app's status bar notification *and* launches the incoming call UI in response * to a new incoming call. * *
If an incoming call is ringing (or call-waiting), the notification will also include a * "fullScreenIntent" that will cause the InCallScreen to be launched, unless the current * foreground activity is marked as "immersive". * *
(This is the mechanism that actually brings up the incoming call UI when we receive a "new * ringing connection" event from the telephony layer.) * *
Also note that this method is safe to call even if the phone isn't actually ringing (or,
* more likely, if an incoming call *was* ringing briefly but then disconnected). In that case,
* we'll simply update or cancel the in-call notification based on the current phone state.
*
* @see #updateInCallNotification()
*/
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void updateNotification() {
updateInCallNotification();
}
/**
* Take down the in-call notification.
*
* @see #updateInCallNotification()
*/
private void cancelNotification() {
if (statusBarCallListener != null) {
setStatusBarCallListener(null);
}
if (currentNotification != NOTIFICATION_NONE) {
TelecomAdapter.getInstance().stopForegroundNotification();
currentNotification = NOTIFICATION_NONE;
}
}
/**
* Helper method for updateInCallNotification() and updateNotification(): Update the phone app's
* status bar notification based on the current telephony state, or cancels the notification if
* the phone is totally idle.
*/
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void updateInCallNotification() {
LogUtil.d("StatusBarNotifier.updateInCallNotification", "");
final DialerCall call = getCallToShow(CallList.getInstance());
if (call != null) {
showNotification(call);
} else {
cancelNotification();
}
}
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void showNotification(final DialerCall call) {
Trace.beginSection("StatusBarNotifier.showNotification");
final boolean isIncoming =
(call.getState() == DialerCallState.INCOMING
|| call.getState() == DialerCallState.CALL_WAITING);
setStatusBarCallListener(new StatusBarCallListener(call));
// we make a call to the contact info cache to query for supplemental data to what the
// call provides. This includes the contact name and photo.
// This callback will always get called immediately and synchronously with whatever data
// it has available, and may make a subsequent call later (same thread) if it had to
// call into the contacts provider for more data.
contactInfoCache.findInfo(call, isIncoming, this);
Trace.endSection();
}
/** Sets up the main Ui for the notification */
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void buildAndSendNotification(
CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
Trace.beginSection("StatusBarNotifier.buildAndSendNotification");
// This can get called to update an existing notification after contact information has come
// back. However, it can happen much later. Before we continue, we need to make sure that
// the call being passed in is still the one we want to show in the notification.
final DialerCall call = getCallToShow(callList);
if (call == null || !call.getId().equals(originalCall.getId())) {
Trace.endSection();
return;
}
Trace.beginSection("prepare work");
final int callState = call.getState();
final CallAudioState callAudioState = AudioModeProvider.getInstance().getAudioState();
Trace.beginSection("read icon and strings");
// Check if data has changed; if nothing is different, don't issue another notification.
final int iconResId = getIconToDisplay(call);
Bitmap largeIcon = getLargeIconToDisplay(context, contactInfo, call);
final CharSequence content = getContentString(call, contactInfo.userType);
final String contentTitle = getContentTitle(contactInfo, call);
Trace.endSection();
final boolean isVideoUpgradeRequest =
call.getVideoTech().getSessionModificationState()
== SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
final int notificationType;
if (callState == DialerCallState.INCOMING
|| callState == DialerCallState.CALL_WAITING
|| isVideoUpgradeRequest) {
if (ConfigProviderComponent.get(context)
.getConfigProvider()
.getBoolean("quiet_incoming_call_if_ui_showing", true)) {
notificationType =
InCallPresenter.getInstance().isShowingInCallUi()
? NOTIFICATION_INCOMING_CALL_QUIET
: NOTIFICATION_INCOMING_CALL;
} else {
boolean alreadyActive =
callList.getActiveOrBackgroundCall() != null
&& InCallPresenter.getInstance().isShowingInCallUi();
notificationType =
alreadyActive ? NOTIFICATION_INCOMING_CALL_QUIET : NOTIFICATION_INCOMING_CALL;
}
} else {
notificationType = NOTIFICATION_IN_CALL;
}
Trace.endSection(); // prepare work
if (!checkForChangeAndSaveData(
iconResId,
content.toString(),
largeIcon,
contentTitle,
callState,
call.getVideoState(),
notificationType,
contactInfo.contactRingtoneUri,
callAudioState)) {
Trace.endSection();
return;
}
if (largeIcon != null) {
largeIcon = getRoundedIcon(largeIcon);
}
// This builder is used for the notification shown when the device is locked and the user
// has set their notification settings to 'hide sensitive content'
// {@see Notification.Builder#setPublicVersion}.
Notification.Builder publicBuilder = new Notification.Builder(context);
publicBuilder
.setSmallIcon(iconResId)
.setColor(ThemeComponent.get(context).theme().getColorPrimary())
// Hide work call state for the lock screen notification
.setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
setNotificationWhen(call, callState, publicBuilder);
// Builder for the notification shown when the device is unlocked or the user has set their
// notification settings to 'show all notification content'.
final Notification.Builder builder = getNotificationBuilder();
builder.setPublicVersion(publicBuilder.build());
// Set up the main intent to send the user to the in-call screen
builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */));
LogUtil.i("StatusBarNotifier.buildAndSendNotification", "notificationType=" + notificationType);
switch (notificationType) {
case NOTIFICATION_INCOMING_CALL:
if (BuildCompat.isAtLeastO()) {
builder.setChannelId(NotificationChannelId.INCOMING_CALL);
}
// Set the intent as a full screen intent as well if a call is incoming
configureFullScreenIntent(builder, createLaunchPendingIntent(true /* isFullScreen */));
// Set the notification category and bump the priority for incoming calls
builder.setCategory(Notification.CATEGORY_CALL);
// This will be ignored on O+ and handled by the channel
builder.setPriority(Notification.PRIORITY_MAX);
if (currentNotification != NOTIFICATION_INCOMING_CALL) {
LogUtil.i(
"StatusBarNotifier.buildAndSendNotification",
"Canceling old notification so this one can be noisy");
// Moving from a non-interuptive notification (or none) to a noisy one. Cancel the old
// notification (if there is one) so the fullScreenIntent or HUN will show
TelecomAdapter.getInstance().stopForegroundNotification();
}
break;
case NOTIFICATION_INCOMING_CALL_QUIET:
if (BuildCompat.isAtLeastO()) {
builder.setChannelId(NotificationChannelId.ONGOING_CALL);
}
break;
case NOTIFICATION_IN_CALL:
if (BuildCompat.isAtLeastO()) {
publicBuilder.setColorized(true);
builder.setColorized(true);
builder.setChannelId(NotificationChannelId.ONGOING_CALL);
}
break;
default:
break;
}
// Set the content
builder.setContentText(content);
builder.setSmallIcon(iconResId);
builder.setContentTitle(contentTitle);
builder.setLargeIcon(largeIcon);
builder.setColor(InCallPresenter.getInstance().getThemeColorManager().getPrimaryColor());
if (isVideoUpgradeRequest) {
builder.setUsesChronometer(false);
addDismissUpgradeRequestAction(builder);
addAcceptUpgradeRequestAction(builder);
} else {
createIncomingCallNotification(call, callState, callAudioState, builder);
}
addPersonReference(builder, contactInfo, call);
Trace.beginSection("fire notification");
// Fire off the notification
Notification notification = builder.build();
if (dialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) {
notification.flags |= Notification.FLAG_INSISTENT;
notification.sound = contactInfo.contactRingtoneUri;
AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder();
audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
notification.audioAttributes = audioAttributes.build();
if (dialerRingtoneManager.shouldVibrate(context.getContentResolver())) {
notification.vibrate = VIBRATE_PATTERN;
}
}
if (dialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone");
dialerRingtoneManager.playCallWaitingTone();
}
LogUtil.i(
"StatusBarNotifier.buildAndSendNotification",
"displaying notification for " + notificationType);
// If a notification exists, this will only update it.
TelecomAdapter.getInstance().startForegroundNotification(NOTIFICATION_ID, notification);
Trace.endSection();
call.getLatencyReport().onNotificationShown();
currentNotification = notificationType;
Trace.endSection();
}
private void createIncomingCallNotification(
DialerCall call, int state, CallAudioState callAudioState, Notification.Builder builder) {
setNotificationWhen(call, state, builder);
// Add hang up option for any active calls (active | onhold), outgoing calls (dialing).
if (state == DialerCallState.ACTIVE
|| state == DialerCallState.ONHOLD
|| DialerCallState.isDialing(state)) {
addHangupAction(builder);
addSpeakerAction(builder, callAudioState);
} else if (state == DialerCallState.INCOMING || state == DialerCallState.CALL_WAITING) {
addDismissAction(builder);
if (call.isVideoCall()) {
addVideoCallAction(builder);
} else {
addAnswerAction(builder);
addSpeakeasyAnswerAction(builder, call);
}
}
}
/**
* Sets the notification's when section as needed. For active calls, this is explicitly set as the
* duration of the call. For all other states, the notification will automatically show the time
* at which the notification was created.
*/
private void setNotificationWhen(DialerCall call, int state, Notification.Builder builder) {
if (state == DialerCallState.ACTIVE) {
builder.setUsesChronometer(true);
builder.setWhen(call.getConnectTimeMillis());
} else {
builder.setUsesChronometer(false);
}
}
/**
* Checks the new notification data and compares it against any notification that we are already
* displaying. If the data is exactly the same, we return false so that we do not issue a new
* notification for the exact same data.
*/
private boolean checkForChangeAndSaveData(
int icon,
String content,
Bitmap largeIcon,
String contentTitle,
int state,
int videoState,
int notificationType,
Uri ringtone,
CallAudioState callAudioState) {
// The two are different:
// if new title is not null, it should be different from saved version OR
// if new title is null, the saved version should not be null
final boolean contentTitleChanged =
(contentTitle != null && !contentTitle.equals(savedContentTitle))
|| (contentTitle == null && savedContentTitle != null);
boolean largeIconChanged;
if (savedLargeIcon == null) {
largeIconChanged = largeIcon != null;
} else {
largeIconChanged = largeIcon == null || !savedLargeIcon.sameAs(largeIcon);
}
// any change means we are definitely updating
boolean retval =
(savedIcon != icon)
|| !Objects.equals(savedContent, content)
|| (callState != state)
|| (this.videoState != videoState)
|| largeIconChanged
|| contentTitleChanged
|| !Objects.equals(this.ringtone, ringtone)
|| !Objects.equals(savedCallAudioState, callAudioState);
LogUtil.d(
"StatusBarNotifier.checkForChangeAndSaveData",
"data changed: icon: %b, content: %b, state: %b, videoState: %b, largeIcon: %b, title: %b,"
+ "ringtone: %b, audioState: %b, type: %b",
(savedIcon != icon),
!Objects.equals(savedContent, content),
(callState != state),
(this.videoState != videoState),
largeIconChanged,
contentTitleChanged,
!Objects.equals(this.ringtone, ringtone),
!Objects.equals(savedCallAudioState, callAudioState),
currentNotification != notificationType);
// If we aren't showing a notification right now or the notification type is changing,
// definitely do an update.
if (currentNotification != notificationType) {
if (currentNotification == NOTIFICATION_NONE) {
LogUtil.d(
"StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time.");
}
retval = true;
}
savedIcon = icon;
savedContent = content;
callState = state;
this.videoState = videoState;
savedLargeIcon = largeIcon;
savedContentTitle = contentTitle;
this.ringtone = ringtone;
savedCallAudioState = callAudioState;
if (retval) {
LogUtil.d(
"StatusBarNotifier.checkForChangeAndSaveData", "data changed. Showing notification");
}
return retval;
}
/** Returns the main string to use in the notification. */
@VisibleForTesting
@Nullable
String getContentTitle(ContactCacheEntry contactInfo, DialerCall call) {
if (call.isConferenceCall()) {
return CallerInfoUtils.getConferenceString(
context, call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE));
}
String preferredName =
ContactsComponent.get(context)
.contactDisplayPreferences()
.getDisplayName(contactInfo.namePrimary, contactInfo.nameAlternative);
if (TextUtils.isEmpty(preferredName)) {
return TextUtils.isEmpty(contactInfo.number)
? null
: BidiFormatter.getInstance()
.unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
}
return preferredName;
}
private void addPersonReference(
Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call) {
// Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
// So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
// NotificationManager using it.
if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
builder.addPerson(contactInfo.lookupUri.toString());
} else if (!TextUtils.isEmpty(call.getNumber())) {
builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null).toString());
}
}
/** Gets a large icon from the contact info object to display in the notification. */
private static Bitmap getLargeIconToDisplay(
Context context, ContactCacheEntry contactInfo, DialerCall call) {
Trace.beginSection("StatusBarNotifier.getLargeIconToDisplay");
Resources resources = context.getResources();
Bitmap largeIcon = null;
if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
}
if (contactInfo.photo == null) {
int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width);
int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height);
@ContactType
int contactType =
LetterTileDrawable.getContactTypeFromPrimitives(
call.isVoiceMailNumber(),
call.isSpam(),
contactInfo.isBusiness,
call.getNumberPresentation(),
call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE));
LetterTileDrawable lettertile = new LetterTileDrawable(resources);
lettertile.setCanonicalDialerLetterTileDetails(
contactInfo.namePrimary == null ? contactInfo.number : contactInfo.namePrimary,
contactInfo.lookupKey,
LetterTileDrawable.SHAPE_CIRCLE,
contactType);
largeIcon = lettertile.getBitmap(width, height);
}
if (call.isSpam()) {
Drawable drawable = resources.getDrawable(R.drawable.blocked_contact, context.getTheme());
largeIcon = DrawableConverter.drawableToBitmap(drawable);
}
Trace.endSection();
return largeIcon;
}
private Bitmap getRoundedIcon(Bitmap bitmap) {
if (bitmap == null) {
return null;
}
final int height =
(int) context.getResources().getDimension(android.R.dimen.notification_large_icon_height);
final int width =
(int) context.getResources().getDimension(android.R.dimen.notification_large_icon_width);
return BitmapUtil.getRoundedBitmap(bitmap, width, height);
}
/**
* Returns the appropriate icon res Id to display based on the call for which we want to display
* information.
*/
@VisibleForTesting
public int getIconToDisplay(DialerCall call) {
// Even if both lines are in use, we only show a single item in
// the expanded Notifications UI. It's labeled "Ongoing call"
// (or "On hold" if there's only one call, and it's on hold.)
// Also, we don't have room to display caller-id info from two
// different calls. So if both lines are in use, display info
// from the foreground call. And if there's a ringing call,
// display that regardless of the state of the other calls.
if (call.getState() == DialerCallState.ONHOLD) {
return R.drawable.quantum_ic_phone_paused_vd_theme_24;
} else if (call.getVideoTech().getSessionModificationState()
== SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST
|| call.isVideoCall()) {
return R.drawable.quantum_ic_videocam_vd_white_24;
} else if (call.hasProperty(PROPERTY_HIGH_DEF_AUDIO)
&& MotorolaUtils.shouldShowHdIconInNotification(context)) {
// Normally when a call is ongoing the status bar displays an icon of a phone. This is a
// helpful hint for users so they know how to get back to the call. For Sprint HD calls, we
// replace this icon with an icon of a phone with a HD badge. This is a carrier requirement.
return R.drawable.ic_hd_call;
} else if (call.hasProperty(Details.PROPERTY_HAS_CDMA_VOICE_PRIVACY)) {
return R.drawable.quantum_ic_phone_locked_vd_theme_24;
}
// If ReturnToCall is enabled, use the static icon. The animated one will show in the bubble.
if (ReturnToCallController.isEnabled(context)) {
return R.drawable.quantum_ic_call_vd_theme_24;
} else {
return R.drawable.on_going_call;
}
}
/** Returns the message to use with the notification. */
private CharSequence getContentString(DialerCall call, @UserType long userType) {
boolean isIncomingOrWaiting =
call.getState() == DialerCallState.INCOMING
|| call.getState() == DialerCallState.CALL_WAITING;
if (isIncomingOrWaiting
&& call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) {
if (!TextUtils.isEmpty(call.getChildNumber())) {
return context.getString(R.string.child_number, call.getChildNumber());
} else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) {
return call.getCallSubject();
}
}
int resId = R.string.notification_ongoing_call;
String wifiBrand = context.getString(R.string.notification_call_wifi_brand);
if (call.hasProperty(Details.PROPERTY_WIFI)) {
resId = R.string.notification_ongoing_call_wifi_template;
}
if (isIncomingOrWaiting) {
if (call.isSpam()) {
resId = R.string.notification_incoming_spam_call;
} else if (shouldShowEnrichedCallNotification(call.getEnrichedCallSession())) {
resId = getECIncomingCallText(call.getEnrichedCallSession());
} else if (call.hasProperty(Details.PROPERTY_WIFI)) {
resId = R.string.notification_incoming_call_wifi_template;
} else if (call.getAccountHandle() != null && hasMultiplePhoneAccounts(call)) {
return getMultiSimIncomingText(call);
} else if (call.isVideoCall()) {
resId = R.string.notification_incoming_video_call;
} else {
resId = R.string.notification_incoming_call;
}
} else if (call.getState() == DialerCallState.ONHOLD) {
resId = R.string.notification_on_hold;
} else if (DialerCallState.isDialing(call.getState())) {
resId = R.string.notification_dialing;
} else if (call.isVideoCall()) {
resId =
call.getVideoTech().isPaused()
? R.string.notification_ongoing_paused_video_call
: R.string.notification_ongoing_video_call;
} else if (call.getVideoTech().getSessionModificationState()
== SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
resId = R.string.notification_requesting_video_call;
}
// Is the call placed through work connection service.
boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL);
if (userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) {
resId = getWorkStringFromPersonalString(resId);
wifiBrand = context.getString(R.string.notification_call_wifi_work_brand);
}
if (resId == R.string.notification_incoming_call_wifi_template
|| resId == R.string.notification_ongoing_call_wifi_template) {
// TODO(a bug): Potentially apply this template logic everywhere.
return context.getString(resId, wifiBrand);
}
return context.getString(resId);
}
private boolean shouldShowEnrichedCallNotification(Session session) {
if (session == null) {
return false;
}
return session.getMultimediaData().hasData() || session.getMultimediaData().isImportant();
}
private int getECIncomingCallText(Session session) {
int resId;
MultimediaData data = session.getMultimediaData();
boolean hasImage = data.hasImageData();
boolean hasSubject = !TextUtils.isEmpty(data.getText());
boolean hasMap = data.getLocation() != null;
if (data.isImportant()) {
if (hasMap) {
if (hasImage) {
if (hasSubject) {
resId = R.string.important_notification_incoming_call_with_photo_message_location;
} else {
resId = R.string.important_notification_incoming_call_with_photo_location;
}
} else if (hasSubject) {
resId = R.string.important_notification_incoming_call_with_message_location;
} else {
resId = R.string.important_notification_incoming_call_with_location;
}
} else if (hasImage) {
if (hasSubject) {
resId = R.string.important_notification_incoming_call_with_photo_message;
} else {
resId = R.string.important_notification_incoming_call_with_photo;
}
} else if (hasSubject) {
resId = R.string.important_notification_incoming_call_with_message;
} else {
resId = R.string.important_notification_incoming_call;
}
if (context.getString(resId).length() > 50) {
resId = R.string.important_notification_incoming_call_attachments;
}
} else {
if (hasMap) {
if (hasImage) {
if (hasSubject) {
resId = R.string.notification_incoming_call_with_photo_message_location;
} else {
resId = R.string.notification_incoming_call_with_photo_location;
}
} else if (hasSubject) {
resId = R.string.notification_incoming_call_with_message_location;
} else {
resId = R.string.notification_incoming_call_with_location;
}
} else if (hasImage) {
if (hasSubject) {
resId = R.string.notification_incoming_call_with_photo_message;
} else {
resId = R.string.notification_incoming_call_with_photo;
}
} else {
resId = R.string.notification_incoming_call_with_message;
}
}
if (context.getString(resId).length() > 50) {
resId = R.string.notification_incoming_call_attachments;
}
return resId;
}
private CharSequence getMultiSimIncomingText(DialerCall call) {
PhoneAccount phoneAccount =
context.getSystemService(TelecomManager.class).getPhoneAccount(call.getAccountHandle());
if (phoneAccount == null) {
return context.getString(R.string.notification_incoming_call);
}
SpannableString string =
new SpannableString(
context.getString(
R.string.notification_incoming_call_mutli_sim, phoneAccount.getLabel()));
int accountStart = string.toString().lastIndexOf(phoneAccount.getLabel().toString());
int accountEnd = accountStart + phoneAccount.getLabel().length();
string.setSpan(
new ForegroundColorSpan(phoneAccount.getHighlightColor()),
accountStart,
accountEnd,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
return string;
}
/** Gets the most relevant call to display in the notification. */
private DialerCall getCallToShow(CallList callList) {
if (callList == null) {
return null;
}
DialerCall call = callList.getIncomingCall();
if (call == null) {
call = callList.getOutgoingCall();
}
if (call == null) {
call = callList.getVideoUpgradeRequestCall();
}
if (call == null) {
call = callList.getActiveOrBackgroundCall();
}
return call;
}
private Spannable getActionText(@StringRes int stringRes, @ColorRes int colorRes) {
Spannable spannable = new SpannableString(context.getText(stringRes));
if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
// This will only work for cases where the Notification.Builder has a fullscreen intent set
// Notification.Builder that does not have a full screen intent will take the color of the
// app and the following leads to a no-op.
spannable.setSpan(
new ForegroundColorSpan(context.getColor(colorRes)), 0, spannable.length(), 0);
}
return spannable;
}
private void addAnswerAction(Notification.Builder builder) {
LogUtil.d(
"StatusBarNotifier.addAnswerAction",
"will show \"answer\" action in the incoming call Notification");
PendingIntent answerVoicePendingIntent =
createNotificationPendingIntent(context, ACTION_ANSWER_VOICE_INCOMING_CALL);
builder.addAction(
new Notification.Action.Builder(
Icon.createWithResource(context, R.drawable.quantum_ic_call_white_24),
getActionText(
R.string.notification_action_answer, R.color.notification_action_accept),
answerVoicePendingIntent)
.build());
}
private void addSpeakeasyAnswerAction(Notification.Builder builder, DialerCall call) {
if (!call.isSpeakEasyEligible()) {
return;
}
if (!ConfigProviderComponent.get(context)
.getConfigProvider()
.getBoolean("enable_speakeasy_notification_button", false)) {
return;
}
if (!SpeakEasyComponent.get(context).speakEasyCallManager().isAvailable(context)) {
return;
}
Optional