blob: 0c1c5a3d9f3ad9830e0e6f804ae9c41a771fcaeb [file] [log] [blame]
/*
* Copyright (C) 2017 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.server.telecom.ui;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telecom.Log;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManagerListenerBase;
import com.android.server.telecom.HandoverState;
import com.android.server.telecom.R;
import com.android.server.telecom.TelecomBroadcastIntentProcessor;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
/**
* Manages the display of an incoming call UX when a new ringing self-managed call is added, and
* there is an ongoing call in another {@link android.telecom.PhoneAccount}.
*/
public class IncomingCallNotifier extends CallsManagerListenerBase {
public interface IncomingCallNotifierFactory {
IncomingCallNotifier make(Context context, CallsManagerProxy mCallsManagerProxy);
}
/**
* Eliminates strict dependency between this class and CallsManager.
*/
public interface CallsManagerProxy {
boolean hasUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle);
int getNumUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle);
Call getActiveCall();
}
// Notification for incoming calls. This is interruptive and will show up as a HUN.
@VisibleForTesting
public static final int NOTIFICATION_INCOMING_CALL = 1;
@VisibleForTesting
public static final String NOTIFICATION_TAG = IncomingCallNotifier.class.getSimpleName();
private final Object mLock = new Object();
public final Call.ListenerBase mCallListener = new Call.ListenerBase() {
@Override
public void onCallerInfoChanged(Call call) {
if (mIncomingCall != call) {
return;
}
showIncomingCallNotification(mIncomingCall);
}
};
private final Context mContext;
private final NotificationManager mNotificationManager;
private final Set<Call> mCalls = new ArraySet<>();
private CallsManagerProxy mCallsManagerProxy;
// The current incoming call we are displaying UX for.
private Call mIncomingCall;
public IncomingCallNotifier(Context context) {
mContext = context;
mNotificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
public void setCallsManagerProxy(CallsManagerProxy callsManagerProxy) {
mCallsManagerProxy = callsManagerProxy;
}
public Call getIncomingCall() {
return mIncomingCall;
}
@Override
public void onCallAdded(Call call) {
synchronized (mLock) {
if (!mCalls.contains(call)) {
mCalls.add(call);
}
}
updateIncomingCall();
}
@Override
public void onCallRemoved(Call call) {
synchronized (mLock) {
if (mCalls.contains(call)) {
mCalls.remove(call);
}
}
updateIncomingCall();
}
@Override
public void onCallStateChanged(Call call, int oldState, int newState) {
updateIncomingCall();
}
/**
* Determines which call is the active ringing call at this time and triggers the display of the
* UI.
*/
private void updateIncomingCall() {
Optional<Call> incomingCallOp;
synchronized (mLock) {
incomingCallOp = mCalls.stream()
.filter(Objects::nonNull)
.filter(call -> call.isSelfManaged() && call.isIncoming() &&
call.getState() == CallState.RINGING &&
call.getHandoverState() == HandoverState.HANDOVER_NONE)
.findFirst();
}
Call incomingCall = incomingCallOp.orElse(null);
if (incomingCall != null && mCallsManagerProxy != null &&
!mCallsManagerProxy.hasUnholdableCallsForOtherConnectionService(
incomingCallOp.get().getTargetPhoneAccount())) {
// If there is no calls in any other ConnectionService, we can rely on the
// third-party app to display its own incoming call UI.
incomingCall = null;
}
Log.i(this, "updateIncomingCall: foundIncomingcall = %s", incomingCall);
boolean hadIncomingCall = mIncomingCall != null;
boolean hasIncomingCall = incomingCall != null;
if (incomingCall != mIncomingCall) {
Call previousIncomingCall = mIncomingCall;
mIncomingCall = incomingCall;
if (hasIncomingCall && !hadIncomingCall) {
mIncomingCall.addListener(mCallListener);
showIncomingCallNotification(mIncomingCall);
} else if (hadIncomingCall && !hasIncomingCall) {
previousIncomingCall.removeListener(mCallListener);
hideIncomingCallNotification();
}
}
}
private void showIncomingCallNotification(Call call) {
Log.i(this, "showIncomingCallNotification showCall = %s", call);
Notification.Builder builder = getNotificationBuilder(call,
mCallsManagerProxy.getActiveCall());
mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL, builder.build());
}
private void hideIncomingCallNotification() {
Log.i(this, "hideIncomingCallNotification");
mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL);
}
private String getNotificationName(Call call) {
String name = "";
if (call.getCallerDisplayNamePresentation() == TelecomManager.PRESENTATION_ALLOWED) {
name = call.getCallerDisplayName();
}
if (TextUtils.isEmpty(name)) {
name = call.getName();
}
if (TextUtils.isEmpty(name)) {
name = call.getPhoneNumber();
}
return name;
}
private Notification.Builder getNotificationBuilder(Call incomingCall, Call ongoingCall) {
// Change the notification app name to "Android System" to sufficiently distinguish this
// from the phone app's name.
Bundle extras = new Bundle();
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, mContext.getString(
com.android.internal.R.string.android_system_label));
Intent answerIntent = new Intent(
TelecomBroadcastIntentProcessor.ACTION_ANSWER_FROM_NOTIFICATION, null, mContext,
TelecomBroadcastReceiver.class);
Intent rejectIntent = new Intent(
TelecomBroadcastIntentProcessor.ACTION_REJECT_FROM_NOTIFICATION, null, mContext,
TelecomBroadcastReceiver.class);
String nameOrNumber = getNotificationName(incomingCall);
CharSequence viaApp = incomingCall.getTargetPhoneAccountLabel();
boolean isIncomingVideo = VideoProfile.isVideo(incomingCall.getVideoState());
boolean isOngoingVideo = ongoingCall != null ?
VideoProfile.isVideo(ongoingCall.getVideoState()) : false;
int numOtherCalls = ongoingCall != null ?
mCallsManagerProxy.getNumUnholdableCallsForOtherConnectionService(
incomingCall.getTargetPhoneAccount()) : 1;
// Build the "IncomingApp call from John Smith" message.
CharSequence incomingCallText;
if (isIncomingVideo) {
incomingCallText = mContext.getString(R.string.notification_incoming_video_call, viaApp,
nameOrNumber);
} else {
incomingCallText = mContext.getString(R.string.notification_incoming_call, viaApp,
nameOrNumber);
}
// Build the "Answering will end your OtherApp call" line.
CharSequence disconnectText;
if (ongoingCall != null && ongoingCall.isSelfManaged()) {
CharSequence ongoingApp = ongoingCall.getTargetPhoneAccountLabel();
// For an ongoing self-managed call, we use a message like:
// "Answering will end your OtherApp call".
if (numOtherCalls > 1) {
// Multiple ongoing calls in the other app, so don't bother specifing whether it is
// a video call or audio call.
disconnectText = mContext.getString(R.string.answering_ends_other_calls,
ongoingApp);
} else if (isOngoingVideo) {
disconnectText = mContext.getString(R.string.answering_ends_other_video_call,
ongoingApp);
} else {
disconnectText = mContext.getString(R.string.answering_ends_other_call, ongoingApp);
}
} else {
// For an ongoing managed call, we use a message like:
// "Answering will end your ongoing call".
if (numOtherCalls > 1) {
// Multiple ongoing manage calls, so don't bother specifing whether it is a video
// call or audio call.
disconnectText = mContext.getString(R.string.answering_ends_other_managed_calls);
} else if (isOngoingVideo) {
disconnectText = mContext.getString(
R.string.answering_ends_other_managed_video_call);
} else {
disconnectText = mContext.getString(R.string.answering_ends_other_managed_call);
}
}
final Notification.Builder builder = new Notification.Builder(mContext);
builder.setOngoing(true);
builder.setExtras(extras);
builder.setPriority(Notification.PRIORITY_HIGH);
builder.setCategory(Notification.CATEGORY_CALL);
builder.setContentTitle(incomingCallText);
builder.setContentText(disconnectText);
builder.setSmallIcon(R.drawable.ic_phone);
builder.setChannelId(NotificationChannelManager.CHANNEL_ID_INCOMING_CALLS);
// Ensures this is a heads up notification. A heads-up notification is typically only shown
// if there is a fullscreen intent. However since this notification doesn't have that we
// will use this trick to get it to show as one anyways.
builder.setVibrate(new long[0]);
builder.setColor(mContext.getResources().getColor(R.color.theme_color));
builder.addAction(
R.anim.on_going_call,
getActionText(R.string.answer_incoming_call, R.color.notification_action_answer),
PendingIntent.getBroadcast(mContext, 0, answerIntent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE));
builder.addAction(
R.drawable.ic_close_dk,
getActionText(R.string.decline_incoming_call, R.color.notification_action_decline),
PendingIntent.getBroadcast(mContext, 0, rejectIntent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE));
return builder;
}
private CharSequence getActionText(int stringRes, int colorRes) {
CharSequence string = mContext.getText(stringRes);
if (string == null) {
return "";
}
Spannable spannable = new SpannableString(string);
spannable.setSpan(
new ForegroundColorSpan(mContext.getColor(colorRes)), 0, spannable.length(), 0);
return spannable;
}
}