/*
* 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_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 android.Manifest;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.Notification.Builder;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
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.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.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.text.BidiFormatter;
import android.text.Spannable;
import android.text.SpannableString;
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.contacts.common.lettertiles.LetterTileDrawable;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.BitmapUtil;
import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.enrichedcall.EnrichedCallManager;
import com.android.dialer.enrichedcall.Session;
import com.android.dialer.multimedia.MultimediaData;
import com.android.dialer.notification.NotificationChannelManager;
import com.android.dialer.notification.NotificationChannelManager.Channel;
import com.android.dialer.oem.MotorolaUtils;
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.call.CallList;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCallListener;
import com.android.incallui.ringtone.DialerRingtoneManager;
import com.android.incallui.ringtone.InCallTonePlayer;
import com.android.incallui.ringtone.ToneGeneratorFactory;
import com.android.incallui.videotech.utils.SessionModificationState;
import java.util.List;
import java.util.Locale;
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 {
// 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 = R.id.notification_ongoing_call;
// Notification for incoming calls. This is interruptive and will show up as a HUN.
private static final int NOTIFICATION_INCOMING_CALL = R.id.notification_incoming_call;
private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0;
private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1;
private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000};
private final Context mContext;
private final ContactInfoCache mContactInfoCache;
private final NotificationManager mNotificationManager;
private final DialerRingtoneManager mDialerRingtoneManager;
@Nullable private ContactsPreferences mContactsPreferences;
private int mCurrentNotification = NOTIFICATION_NONE;
private int mCallState = DialerCall.State.INVALID;
private int mSavedIcon = 0;
private String mSavedContent = null;
private Bitmap mSavedLargeIcon;
private String mSavedContentTitle;
private Uri mRingtone;
private StatusBarCallListener mStatusBarCallListener;
public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
Objects.requireNonNull(context);
mContext = context;
mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
mContactInfoCache = contactInfoCache;
mNotificationManager = context.getSystemService(NotificationManager.class);
mDialerRingtoneManager =
new DialerRingtoneManager(
new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()),
CallList.getInstance());
mCurrentNotification = NOTIFICATION_NONE;
}
/**
* Should only be called from a irrecoverable state where it is necessary to dismiss all
* notifications.
*/
static void clearAllCallNotifications(Context backupContext) {
LogUtil.i(
"StatusBarNotifier.clearAllCallNotifications",
"something terrible happened, clear all InCall notifications");
NotificationManager notificationManager =
backupContext.getSystemService(NotificationManager.class);
notificationManager.cancel(NOTIFICATION_IN_CALL);
notificationManager.cancel(NOTIFICATION_INCOMING_CALL);
}
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_ongoing_call_wifi) {
return R.string.notification_ongoing_work_call_wifi;
} else if (resId == R.string.notification_incoming_call_wifi) {
return R.string.notification_incoming_work_call_wifi;
} 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);
}
private static void setColorized(@NonNull Builder builder) {
if (BuildCompat.isAtLeastO()) {
builder.setColorized(true);
}
}
/** 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(callList);
}
@Override
public void onEnrichedCallStateChanged() {
LogUtil.enterBlock("StatusBarNotifier.onEnrichedCallStateChanged");
updateNotification(CallList.getInstance());
}
/**
* 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(CallList)
*/
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void updateNotification(CallList callList) {
updateInCallNotification(callList);
}
/**
* Take down the in-call notification.
*
* @see #updateInCallNotification(CallList)
*/
private void cancelNotification() {
if (mStatusBarCallListener != null) {
setStatusBarCallListener(null);
}
if (mCurrentNotification != NOTIFICATION_NONE) {
LogUtil.i("StatusBarNotifier.cancelNotification", "cancel");
mNotificationManager.cancel(mCurrentNotification);
}
mCurrentNotification = 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(CallList callList) {
LogUtil.d("StatusBarNotifier.updateInCallNotification", "");
final DialerCall call = getCallToShow(callList);
if (call != null) {
showNotification(callList, call);
} else {
cancelNotification();
}
}
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void showNotification(final CallList callList, final DialerCall call) {
final boolean isIncoming =
(call.getState() == DialerCall.State.INCOMING
|| call.getState() == DialerCall.State.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.
mContactInfoCache.findInfo(
call,
isIncoming,
new ContactInfoCacheCallback() {
@Override
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
DialerCall call = callList.getCallById(callId);
if (call != null) {
call.getLogState().contactLookupResult = entry.contactLookupResult;
buildAndSendNotification(callList, call, entry);
}
}
@Override
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
DialerCall call = callList.getCallById(callId);
if (call != null) {
buildAndSendNotification(callList, call, entry);
}
}
});
}
/** Sets up the main Ui for the notification */
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void buildAndSendNotification(
CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
// 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())) {
return;
}
final int callState = call.getState();
// Check if data has changed; if nothing is different, don't issue another notification.
final int iconResId = getIconToDisplay(call);
Bitmap largeIcon = getLargeIconToDisplay(contactInfo, call);
final String content = getContentString(call, contactInfo.userType);
final String contentTitle = getContentTitle(contactInfo, call);
final boolean isVideoUpgradeRequest =
call.getVideoTech().getSessionModificationState()
== SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
final int notificationType;
if (callState == DialerCall.State.INCOMING
|| callState == DialerCall.State.CALL_WAITING
|| isVideoUpgradeRequest) {
notificationType = NOTIFICATION_INCOMING_CALL;
} else {
notificationType = NOTIFICATION_IN_CALL;
}
if (!checkForChangeAndSaveData(
iconResId,
content,
largeIcon,
contentTitle,
callState,
notificationType,
contactInfo.contactRingtoneUri)) {
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(mContext);
publicBuilder
.setSmallIcon(iconResId)
.setColor(mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()))
// Hide work call state for the lock screen notification
.setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
setColorized(publicBuilder);
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 */));
// Set the intent as a full screen intent as well if a call is incoming
PhoneAccountHandle accountHandle = call.getAccountHandle();
if (accountHandle == null) {
accountHandle = getAnyPhoneAccount();
}
if (notificationType == NOTIFICATION_INCOMING_CALL) {
NotificationChannelManager.applyChannel(
builder, mContext, Channel.INCOMING_CALL, accountHandle);
configureFullScreenIntent(
builder, createLaunchPendingIntent(true /* isFullScreen */), callList, call);
// Set the notification category and bump the priority for incoming calls
builder.setCategory(Notification.CATEGORY_CALL);
builder.setPriority(Notification.PRIORITY_MAX);
} else {
NotificationChannelManager.applyChannel(
builder, mContext, Channel.ONGOING_CALL, accountHandle);
}
// Set the content
builder.setContentText(content);
builder.setSmallIcon(iconResId);
builder.setContentTitle(contentTitle);
builder.setLargeIcon(largeIcon);
builder.setColor(
mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()));
setColorized(builder);
if (isVideoUpgradeRequest) {
builder.setUsesChronometer(false);
addDismissUpgradeRequestAction(builder);
addAcceptUpgradeRequestAction(builder);
} else {
createIncomingCallNotification(call, callState, builder);
}
addPersonReference(builder, contactInfo, call);
// Fire off the notification
Notification notification = builder.build();
if (mDialerRingtoneManager.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 (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) {
notification.vibrate = VIBRATE_PATTERN;
}
}
if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone");
mDialerRingtoneManager.playCallWaitingTone();
}
if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
LogUtil.i(
"StatusBarNotifier.buildAndSendNotification",
"previous notification already showing - cancelling " + mCurrentNotification);
mNotificationManager.cancel(mCurrentNotification);
}
LogUtil.i(
"StatusBarNotifier.buildAndSendNotification",
"displaying notification for " + notificationType);
try {
mNotificationManager.notify(notificationType, notification);
} catch (RuntimeException e) {
// TODO(b/34744003): Move the memory stats into silent feedback PSD.
ActivityManager activityManager = mContext.getSystemService(ActivityManager.class);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
throw new RuntimeException(
String.format(
Locale.US,
"Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
contactInfo.photoType,
memoryInfo.lowMemory,
memoryInfo.availMem),
e);
}
call.getLatencyReport().onNotificationShown();
mCurrentNotification = notificationType;
}
@Nullable
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private PhoneAccountHandle getAnyPhoneAccount() {
PhoneAccountHandle accountHandle;
TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
accountHandle = telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
if (accountHandle == null) {
List accountHandles = telecomManager.getCallCapablePhoneAccounts();
if (!accountHandles.isEmpty()) {
accountHandle = accountHandles.get(0);
}
}
return accountHandle;
}
private void createIncomingCallNotification(
DialerCall call, int state, Notification.Builder builder) {
setNotificationWhen(call, state, builder);
// Add hang up option for any active calls (active | onhold), outgoing calls (dialing).
if (state == DialerCall.State.ACTIVE
|| state == DialerCall.State.ONHOLD
|| DialerCall.State.isDialing(state)) {
addHangupAction(builder);
} else if (state == DialerCall.State.INCOMING || state == DialerCall.State.CALL_WAITING) {
addDismissAction(builder);
if (call.isVideoCall()) {
addVideoCallAction(builder);
} else {
addAnswerAction(builder);
}
}
}
/**
* 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 == DialerCall.State.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 notificationType,
Uri ringtone) {
// 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(mSavedContentTitle))
|| (contentTitle == null && mSavedContentTitle != null);
boolean largeIconChanged =
mSavedLargeIcon == null ? largeIcon != null : !mSavedLargeIcon.sameAs(largeIcon);
// any change means we are definitely updating
boolean retval =
(mSavedIcon != icon)
|| !Objects.equals(mSavedContent, content)
|| (mCallState != state)
|| largeIconChanged
|| contentTitleChanged
|| !Objects.equals(mRingtone, ringtone);
// If we aren't showing a notification right now or the notification type is changing,
// definitely do an update.
if (mCurrentNotification != notificationType) {
if (mCurrentNotification == NOTIFICATION_NONE) {
LogUtil.d(
"StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time.");
}
retval = true;
}
mSavedIcon = icon;
mSavedContent = content;
mCallState = state;
mSavedLargeIcon = largeIcon;
mSavedContentTitle = contentTitle;
mRingtone = ringtone;
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() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
return mContext.getResources().getString(R.string.conference_call_name);
}
String preferredName =
ContactDisplayUtils.getPreferredDisplayName(
contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences);
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 Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, DialerCall call) {
Bitmap largeIcon = null;
if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
largeIcon = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.img_conference);
}
if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
}
if (contactInfo.photo == null) {
int width =
(int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width);
int height =
(int)
mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
int contactType = LetterTileDrawable.TYPE_DEFAULT;
LetterTileDrawable lettertile = new LetterTileDrawable(mContext.getResources());
// TODO: Deduplicate across Dialer. b/36195917
if (CallerInfoUtils.isVoiceMailNumber(mContext, call)) {
contactType = LetterTileDrawable.TYPE_VOICEMAIL;
} else if (contactInfo.isBusiness) {
contactType = LetterTileDrawable.TYPE_BUSINESS;
} else if (call.getNumberPresentation() == TelecomManager.PRESENTATION_RESTRICTED) {
contactType = LetterTileDrawable.TYPE_GENERIC_AVATAR;
}
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 =
mContext.getResources().getDrawable(R.drawable.blocked_contact, mContext.getTheme());
largeIcon = DrawableConverter.drawableToBitmap(drawable);
}
return largeIcon;
}
private Bitmap getRoundedIcon(Bitmap bitmap) {
if (bitmap == null) {
return null;
}
final int height =
(int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
final int width =
(int) mContext.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.
*/
private 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() == DialerCall.State.ONHOLD) {
return R.drawable.ic_phone_paused_white_24dp;
} else if (call.getVideoTech().getSessionModificationState()
== SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
return R.drawable.quantum_ic_videocam_white_24;
} else if (call.hasProperty(PROPERTY_HIGH_DEF_AUDIO)
&& MotorolaUtils.shouldShowHdIconInNotification(mContext)) {
// Normally when a call is ongoing the status bar displays an icon of a phone with animated
// lines. 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;
}
return R.anim.on_going_call;
}
/** Returns the message to use with the notification. */
private String getContentString(DialerCall call, @UserType long userType) {
boolean isIncomingOrWaiting =
call.getState() == DialerCall.State.INCOMING
|| call.getState() == DialerCall.State.CALL_WAITING;
if (isIncomingOrWaiting
&& call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) {
if (!TextUtils.isEmpty(call.getChildNumber())) {
return mContext.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;
if (call.hasProperty(Details.PROPERTY_WIFI)) {
resId = R.string.notification_ongoing_call_wifi;
}
if (isIncomingOrWaiting) {
EnrichedCallManager manager = EnrichedCallComponent.get(mContext).getEnrichedCallManager();
Session session = null;
if (call.getNumber() != null) {
session = manager.getSession(call.getUniqueCallId(), call.getNumber());
}
if (call.isSpam()) {
resId = R.string.notification_incoming_spam_call;
} else if (session != null) {
resId = getECIncomingCallText(session);
} else if (call.hasProperty(Details.PROPERTY_WIFI)) {
resId = R.string.notification_incoming_call_wifi;
} else {
resId = R.string.notification_incoming_call;
}
} else if (call.getState() == DialerCall.State.ONHOLD) {
resId = R.string.notification_on_hold;
} else if (DialerCall.State.isDialing(call.getState())) {
resId = R.string.notification_dialing;
} 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);
}
return mContext.getString(resId);
}
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 {
resId = R.string.important_notification_incoming_call_with_message;
}
if (mContext.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 (mContext.getString(resId).length() > 50) {
resId = R.string.notification_incoming_call_attachments;
}
return resId;
}
/** 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(mContext.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(mContext.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(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
// We put animation resources in "anim" folder instead of "drawable", which causes Android
// Studio to complain.
// TODO: Move "anim" resources to "drawable" as recommended in AnimationDrawable doc?
//noinspection ResourceType
builder.addAction(
new Notification.Action.Builder(
Icon.createWithResource(mContext, R.anim.on_going_call),
getActionText(
R.string.notification_action_answer, R.color.notification_action_accept),
answerVoicePendingIntent)
.build());
}
private void addDismissAction(Notification.Builder builder) {
LogUtil.d(
"StatusBarNotifier.addDismissAction",
"will show \"decline\" action in the incoming call Notification");
PendingIntent declinePendingIntent =
createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
builder.addAction(
new Notification.Action.Builder(
Icon.createWithResource(mContext, R.drawable.quantum_ic_close_white_24),
getActionText(
R.string.notification_action_dismiss, R.color.notification_action_dismiss),
declinePendingIntent)
.build());
}
private void addHangupAction(Notification.Builder builder) {
LogUtil.d(
"StatusBarNotifier.addHangupAction",
"will show \"hang-up\" action in the ongoing active call Notification");
PendingIntent hangupPendingIntent =
createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
builder.addAction(
new Notification.Action.Builder(
Icon.createWithResource(mContext, R.drawable.ic_call_end_white_24dp),
getActionText(
R.string.notification_action_end_call, R.color.notification_action_end_call),
hangupPendingIntent)
.build());
}
private void addVideoCallAction(Notification.Builder builder) {
LogUtil.i(
"StatusBarNotifier.addVideoCallAction",
"will show \"video\" action in the incoming call Notification");
PendingIntent answerVideoPendingIntent =
createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
builder.addAction(
new Notification.Action.Builder(
Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24),
getActionText(
R.string.notification_action_answer_video,
R.color.notification_action_answer_video),
answerVideoPendingIntent)
.build());
}
private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
LogUtil.i(
"StatusBarNotifier.addAcceptUpgradeRequestAction",
"will show \"accept upgrade\" action in the incoming call Notification");
PendingIntent acceptVideoPendingIntent =
createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
builder.addAction(
new Notification.Action.Builder(
Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24),
getActionText(
R.string.notification_action_accept, R.color.notification_action_accept),
acceptVideoPendingIntent)
.build());
}
private void addDismissUpgradeRequestAction(Notification.Builder builder) {
LogUtil.i(
"StatusBarNotifier.addDismissUpgradeRequestAction",
"will show \"dismiss upgrade\" action in the incoming call Notification");
PendingIntent declineVideoPendingIntent =
createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
builder.addAction(
new Notification.Action.Builder(
Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24),
getActionText(
R.string.notification_action_dismiss, R.color.notification_action_dismiss),
declineVideoPendingIntent)
.build());
}
/** Adds fullscreen intent to the builder. */
private void configureFullScreenIntent(
Notification.Builder builder, PendingIntent intent, CallList callList, DialerCall call) {
// Ok, we actually want to launch the incoming call
// UI at this point (in addition to simply posting a notification
// to the status bar). Setting fullScreenIntent will cause
// the InCallScreen to be launched immediately *unless* the
// current foreground activity is marked as "immersive".
LogUtil.d("StatusBarNotifier.configureFullScreenIntent", "setting fullScreenIntent: " + intent);
builder.setFullScreenIntent(intent, true);
// Ugly hack alert:
//
// The NotificationManager has the (undocumented) behavior
// that it will *ignore* the fullScreenIntent field if you
// post a new Notification that matches the ID of one that's
// already active. Unfortunately this is exactly what happens
// when you get an incoming call-waiting call: the
// "ongoing call" notification is already visible, so the
// InCallScreen won't get launched in this case!
// (The result: if you bail out of the in-call UI while on a
// call and then get a call-waiting call, the incoming call UI
// won't come up automatically.)
//
// The workaround is to just notice this exact case (this is a
// call-waiting call *and* the InCallScreen is not in the
// foreground) and manually cancel the in-call notification
// before (re)posting it.
//
// TODO: there should be a cleaner way of avoiding this
// problem (see discussion in bug 3184149.)
// If a call is onhold during an incoming call, the call actually comes in as
// INCOMING. For that case *and* traditional call-waiting, we want to
// cancel the notification.
boolean isCallWaiting =
(call.getState() == DialerCall.State.CALL_WAITING
|| (call.getState() == DialerCall.State.INCOMING
&& callList.getBackgroundCall() != null));
if (isCallWaiting) {
LogUtil.i(
"StatusBarNotifier.configureFullScreenIntent",
"updateInCallNotification: call-waiting! force relaunch...");
// Cancel the IN_CALL_NOTIFICATION immediately before
// (re)posting it; this seems to force the
// NotificationManager to launch the fullScreenIntent.
mNotificationManager.cancel(NOTIFICATION_IN_CALL);
}
}
private Notification.Builder getNotificationBuilder() {
final Notification.Builder builder = new Notification.Builder(mContext);
builder.setOngoing(true);
builder.setOnlyAlertOnce(true);
return builder;
}
private PendingIntent createLaunchPendingIntent(boolean isFullScreen) {
Intent intent =
InCallActivity.getIntent(
mContext, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen);
int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN;
if (isFullScreen) {
// Use a unique request code so that the pending intent isn't clobbered by the
// non-full screen pending intent.
requestCode = PENDING_INTENT_REQUEST_CODE_FULL_SCREEN;
}
// PendingIntent that can be used to launch the InCallActivity. The
// system fires off this intent if the user pulls down the windowshade
// and clicks the notification's expanded view. It's also used to
// launch the InCallActivity immediately when when there's an incoming
// call (see the "fullScreenIntent" field below).
return PendingIntent.getActivity(mContext, requestCode, intent, 0);
}
private void setStatusBarCallListener(StatusBarCallListener listener) {
if (mStatusBarCallListener != null) {
mStatusBarCallListener.cleanup();
}
mStatusBarCallListener = listener;
}
private class StatusBarCallListener implements DialerCallListener {
private DialerCall mDialerCall;
StatusBarCallListener(DialerCall dialerCall) {
mDialerCall = dialerCall;
mDialerCall.addListener(this);
}
void cleanup() {
mDialerCall.removeListener(this);
}
@Override
public void onDialerCallDisconnect() {}
@Override
public void onDialerCallUpdate() {
if (CallList.getInstance().getIncomingCall() == null) {
mDialerRingtoneManager.stopCallWaitingTone();
}
}
@Override
public void onDialerCallChildNumberChange() {}
@Override
public void onDialerCallLastForwardedNumberChange() {}
@Override
public void onDialerCallUpgradeToVideo() {}
@Override
public void onWiFiToLteHandover() {}
@Override
public void onHandoverToWifiFailure() {}
/**
* Responds to changes in the session modification state for the call by dismissing the status
* bar notification as required.
*/
@Override
public void onDialerCallSessionModificationStateChange() {
if (mDialerCall.getVideoTech().getSessionModificationState()
== SessionModificationState.NO_REQUEST) {
cleanup();
updateNotification(CallList.getInstance());
}
}
}
}