You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1166 lines
47 KiB
1166 lines
47 KiB
4 months ago
|
/*
|
||
|
* 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.
|
||
|
*
|
||
|
* <p>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".
|
||
|
*
|
||
|
* <p>(This is the mechanism that actually brings up the incoming call UI when we receive a "new
|
||
|
* ringing connection" event from the telephony layer.)
|
||
|
*
|
||
|
* <p>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<Integer> buttonText = SpeakEasyComponent.get(context).speakEasyTextResource();
|
||
|
if (!buttonText.isPresent()) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
LogUtil.d("StatusBarNotifier.addSpeakeasyAnswerAction", "showing button");
|
||
|
PendingIntent answerVoicePendingIntent =
|
||
|
createNotificationPendingIntent(context, ACTION_ANSWER_SPEAKEASY_CALL);
|
||
|
|
||
|
Spannable spannable = new SpannableString(context.getText(buttonText.get()));
|
||
|
// TODO(erfanian): Migrate these color values to somewhere more permanent in subsequent
|
||
|
// implementation.
|
||
|
spannable.setSpan(
|
||
|
new ForegroundColorSpan(
|
||
|
context.getColor(R.color.DO_NOT_USE_OR_I_WILL_BREAK_YOU_text_span_tertiary_button)),
|
||
|
0,
|
||
|
spannable.length(),
|
||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||
|
|
||
|
builder.addAction(
|
||
|
new Notification.Action.Builder(
|
||
|
Icon.createWithResource(context, R.drawable.quantum_ic_call_white_24),
|
||
|
spannable,
|
||
|
answerVoicePendingIntent)
|
||
|
.build());
|
||
|
}
|
||
|
|
||
|
private void addDismissAction(Notification.Builder builder) {
|
||
|
LogUtil.d(
|
||
|
"StatusBarNotifier.addDismissAction",
|
||
|
"will show \"decline\" action in the incoming call Notification");
|
||
|
PendingIntent declinePendingIntent =
|
||
|
createNotificationPendingIntent(context, ACTION_DECLINE_INCOMING_CALL);
|
||
|
builder.addAction(
|
||
|
new Notification.Action.Builder(
|
||
|
Icon.createWithResource(context, 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(context, ACTION_HANG_UP_ONGOING_CALL);
|
||
|
builder.addAction(
|
||
|
new Notification.Action.Builder(
|
||
|
Icon.createWithResource(context, R.drawable.quantum_ic_call_end_white_24),
|
||
|
context.getText(R.string.notification_action_end_call),
|
||
|
hangupPendingIntent)
|
||
|
.build());
|
||
|
}
|
||
|
|
||
|
private void addSpeakerAction(Notification.Builder builder, CallAudioState callAudioState) {
|
||
|
if ((callAudioState.getSupportedRouteMask() & CallAudioState.ROUTE_BLUETOOTH)
|
||
|
== CallAudioState.ROUTE_BLUETOOTH) {
|
||
|
// Don't add speaker button if bluetooth is connected
|
||
|
return;
|
||
|
}
|
||
|
if (callAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
|
||
|
addSpeakerOffAction(builder);
|
||
|
} else if ((callAudioState.getRoute() & CallAudioState.ROUTE_WIRED_OR_EARPIECE) != 0) {
|
||
|
addSpeakerOnAction(builder);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void addSpeakerOnAction(Notification.Builder builder) {
|
||
|
LogUtil.d(
|
||
|
"StatusBarNotifier.addSpeakerOnAction",
|
||
|
"will show \"Speaker on\" action in the ongoing active call Notification");
|
||
|
PendingIntent speakerOnPendingIntent =
|
||
|
createNotificationPendingIntent(context, ACTION_TURN_ON_SPEAKER);
|
||
|
builder.addAction(
|
||
|
new Notification.Action.Builder(
|
||
|
Icon.createWithResource(context, R.drawable.quantum_ic_volume_up_vd_theme_24),
|
||
|
context.getText(R.string.notification_action_speaker_on),
|
||
|
speakerOnPendingIntent)
|
||
|
.build());
|
||
|
}
|
||
|
|
||
|
private void addSpeakerOffAction(Notification.Builder builder) {
|
||
|
LogUtil.d(
|
||
|
"StatusBarNotifier.addSpeakerOffAction",
|
||
|
"will show \"Speaker off\" action in the ongoing active call Notification");
|
||
|
PendingIntent speakerOffPendingIntent =
|
||
|
createNotificationPendingIntent(context, ACTION_TURN_OFF_SPEAKER);
|
||
|
builder.addAction(
|
||
|
new Notification.Action.Builder(
|
||
|
Icon.createWithResource(context, R.drawable.quantum_ic_phone_in_talk_vd_theme_24),
|
||
|
context.getText(R.string.notification_action_speaker_off),
|
||
|
speakerOffPendingIntent)
|
||
|
.build());
|
||
|
}
|
||
|
|
||
|
private void addVideoCallAction(Notification.Builder builder) {
|
||
|
LogUtil.i(
|
||
|
"StatusBarNotifier.addVideoCallAction",
|
||
|
"will show \"video\" action in the incoming call Notification");
|
||
|
PendingIntent answerVideoPendingIntent =
|
||
|
createNotificationPendingIntent(context, ACTION_ANSWER_VIDEO_INCOMING_CALL);
|
||
|
builder.addAction(
|
||
|
new Notification.Action.Builder(
|
||
|
Icon.createWithResource(context, R.drawable.quantum_ic_videocam_vd_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(context, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
|
||
|
builder.addAction(
|
||
|
new Notification.Action.Builder(
|
||
|
Icon.createWithResource(context, R.drawable.quantum_ic_videocam_vd_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(context, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
|
||
|
builder.addAction(
|
||
|
new Notification.Action.Builder(
|
||
|
Icon.createWithResource(context, R.drawable.quantum_ic_videocam_vd_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) {
|
||
|
// 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);
|
||
|
}
|
||
|
|
||
|
private Notification.Builder getNotificationBuilder() {
|
||
|
final Notification.Builder builder = new Notification.Builder(context);
|
||
|
builder.setOngoing(true);
|
||
|
builder.setOnlyAlertOnce(true);
|
||
|
// This will be ignored on O+ and handled by the channel
|
||
|
// noinspection deprecation
|
||
|
builder.setPriority(Notification.PRIORITY_HIGH);
|
||
|
|
||
|
return builder;
|
||
|
}
|
||
|
|
||
|
private PendingIntent createLaunchPendingIntent(boolean isFullScreen) {
|
||
|
Intent intent =
|
||
|
InCallActivity.getIntent(
|
||
|
context, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen);
|
||
|
|
||
|
int requestCode = InCallActivity.PendingIntentRequestCodes.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 = InCallActivity.PendingIntentRequestCodes.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(context, requestCode, intent, 0);
|
||
|
}
|
||
|
|
||
|
private void setStatusBarCallListener(StatusBarCallListener listener) {
|
||
|
if (statusBarCallListener != null) {
|
||
|
statusBarCallListener.cleanup();
|
||
|
}
|
||
|
statusBarCallListener = listener;
|
||
|
}
|
||
|
|
||
|
private boolean hasMultiplePhoneAccounts(DialerCall call) {
|
||
|
if (call.getCallCapableAccounts() == null) {
|
||
|
return false;
|
||
|
}
|
||
|
return call.getCallCapableAccounts().size() > 1;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
|
||
|
public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
|
||
|
DialerCall call = CallList.getInstance().getCallById(callId);
|
||
|
if (call != null) {
|
||
|
call.getLogState().contactLookupResult = entry.contactLookupResult;
|
||
|
buildAndSendNotification(CallList.getInstance(), call, entry);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
|
||
|
public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
|
||
|
DialerCall call = CallList.getInstance().getCallById(callId);
|
||
|
if (call != null) {
|
||
|
buildAndSendNotification(CallList.getInstance(), call, entry);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private class StatusBarCallListener implements DialerCallListener {
|
||
|
|
||
|
private DialerCall dialerCall;
|
||
|
|
||
|
StatusBarCallListener(DialerCall dialerCall) {
|
||
|
this.dialerCall = dialerCall;
|
||
|
this.dialerCall.addListener(this);
|
||
|
}
|
||
|
|
||
|
void cleanup() {
|
||
|
dialerCall.removeListener(this);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onDialerCallDisconnect() {}
|
||
|
|
||
|
@Override
|
||
|
public void onDialerCallUpdate() {
|
||
|
if (CallList.getInstance().getIncomingCall() == null) {
|
||
|
dialerRingtoneManager.stopCallWaitingTone();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onDialerCallChildNumberChange() {}
|
||
|
|
||
|
@Override
|
||
|
public void onDialerCallLastForwardedNumberChange() {}
|
||
|
|
||
|
@Override
|
||
|
public void onDialerCallUpgradeToVideo() {}
|
||
|
|
||
|
@Override
|
||
|
public void onWiFiToLteHandover() {}
|
||
|
|
||
|
@Override
|
||
|
public void onHandoverToWifiFailure() {}
|
||
|
|
||
|
@Override
|
||
|
public void onInternationalCallOnWifi() {}
|
||
|
|
||
|
@Override
|
||
|
public void onEnrichedCallSessionUpdate() {}
|
||
|
|
||
|
/**
|
||
|
* Responds to changes in the session modification state for the call by dismissing the status
|
||
|
* bar notification as required.
|
||
|
*/
|
||
|
@Override
|
||
|
public void onDialerCallSessionModificationStateChange() {
|
||
|
if (dialerCall.getVideoTech().getSessionModificationState()
|
||
|
== SessionModificationState.NO_REQUEST) {
|
||
|
cleanup();
|
||
|
updateNotification();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|