/* * 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; } }