1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.server.telecom.ui; 18 19 import android.app.Notification; 20 import android.app.NotificationManager; 21 import android.app.PendingIntent; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.os.Bundle; 25 import android.telecom.Log; 26 import android.telecom.PhoneAccountHandle; 27 import android.telecom.TelecomManager; 28 import android.telecom.VideoProfile; 29 import android.text.Spannable; 30 import android.text.SpannableString; 31 import android.text.TextUtils; 32 import android.text.style.ForegroundColorSpan; 33 import android.util.ArraySet; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.server.telecom.Call; 37 import com.android.server.telecom.CallState; 38 import com.android.server.telecom.CallsManagerListenerBase; 39 import com.android.server.telecom.HandoverState; 40 import com.android.server.telecom.R; 41 import com.android.server.telecom.TelecomBroadcastIntentProcessor; 42 import com.android.server.telecom.components.TelecomBroadcastReceiver; 43 44 import java.util.Objects; 45 import java.util.Optional; 46 import java.util.Set; 47 48 /** 49 * Manages the display of an incoming call UX when a new ringing self-managed call is added, and 50 * there is an ongoing call in another {@link android.telecom.PhoneAccount}. 51 */ 52 public class IncomingCallNotifier extends CallsManagerListenerBase { 53 54 public interface IncomingCallNotifierFactory { make(Context context, CallsManagerProxy mCallsManagerProxy)55 IncomingCallNotifier make(Context context, CallsManagerProxy mCallsManagerProxy); 56 } 57 58 /** 59 * Eliminates strict dependency between this class and CallsManager. 60 */ 61 public interface CallsManagerProxy { hasUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle)62 boolean hasUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle); getNumUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle)63 int getNumUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle); getActiveCall()64 Call getActiveCall(); 65 } 66 67 // Notification for incoming calls. This is interruptive and will show up as a HUN. 68 @VisibleForTesting 69 public static final int NOTIFICATION_INCOMING_CALL = 1; 70 @VisibleForTesting 71 public static final String NOTIFICATION_TAG = IncomingCallNotifier.class.getSimpleName(); 72 private final Object mLock = new Object(); 73 74 public final Call.ListenerBase mCallListener = new Call.ListenerBase() { 75 @Override 76 public void onCallerInfoChanged(Call call) { 77 if (mIncomingCall != call) { 78 return; 79 } 80 showIncomingCallNotification(mIncomingCall); 81 } 82 }; 83 84 private final Context mContext; 85 private final NotificationManager mNotificationManager; 86 private final Set<Call> mCalls = new ArraySet<>(); 87 private CallsManagerProxy mCallsManagerProxy; 88 89 // The current incoming call we are displaying UX for. 90 private Call mIncomingCall; 91 IncomingCallNotifier(Context context)92 public IncomingCallNotifier(Context context) { 93 mContext = context; 94 mNotificationManager = 95 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 96 } 97 setCallsManagerProxy(CallsManagerProxy callsManagerProxy)98 public void setCallsManagerProxy(CallsManagerProxy callsManagerProxy) { 99 mCallsManagerProxy = callsManagerProxy; 100 } 101 getIncomingCall()102 public Call getIncomingCall() { 103 return mIncomingCall; 104 } 105 106 @Override onCallAdded(Call call)107 public void onCallAdded(Call call) { 108 synchronized (mLock) { 109 if (!mCalls.contains(call)) { 110 mCalls.add(call); 111 } 112 } 113 114 updateIncomingCall(); 115 } 116 117 @Override onCallRemoved(Call call)118 public void onCallRemoved(Call call) { 119 synchronized (mLock) { 120 if (mCalls.contains(call)) { 121 mCalls.remove(call); 122 } 123 } 124 updateIncomingCall(); 125 } 126 127 @Override onCallStateChanged(Call call, int oldState, int newState)128 public void onCallStateChanged(Call call, int oldState, int newState) { 129 updateIncomingCall(); 130 } 131 132 /** 133 * Determines which call is the active ringing call at this time and triggers the display of the 134 * UI. 135 */ updateIncomingCall()136 private void updateIncomingCall() { 137 Optional<Call> incomingCallOp; 138 synchronized (mLock) { 139 incomingCallOp = mCalls.stream() 140 .filter(Objects::nonNull) 141 .filter(call -> call.isSelfManaged() && call.isIncoming() && 142 call.getState() == CallState.RINGING && 143 call.getHandoverState() == HandoverState.HANDOVER_NONE) 144 .findFirst(); 145 } 146 147 Call incomingCall = incomingCallOp.orElse(null); 148 if (incomingCall != null && mCallsManagerProxy != null && 149 !mCallsManagerProxy.hasUnholdableCallsForOtherConnectionService( 150 incomingCallOp.get().getTargetPhoneAccount())) { 151 // If there is no calls in any other ConnectionService, we can rely on the 152 // third-party app to display its own incoming call UI. 153 incomingCall = null; 154 } 155 156 Log.i(this, "updateIncomingCall: foundIncomingcall = %s", incomingCall); 157 158 boolean hadIncomingCall = mIncomingCall != null; 159 boolean hasIncomingCall = incomingCall != null; 160 if (incomingCall != mIncomingCall) { 161 Call previousIncomingCall = mIncomingCall; 162 mIncomingCall = incomingCall; 163 164 if (hasIncomingCall && !hadIncomingCall) { 165 mIncomingCall.addListener(mCallListener); 166 showIncomingCallNotification(mIncomingCall); 167 } else if (hadIncomingCall && !hasIncomingCall) { 168 previousIncomingCall.removeListener(mCallListener); 169 hideIncomingCallNotification(); 170 } 171 } 172 } 173 showIncomingCallNotification(Call call)174 private void showIncomingCallNotification(Call call) { 175 Log.i(this, "showIncomingCallNotification showCall = %s", call); 176 177 Notification.Builder builder = getNotificationBuilder(call, 178 mCallsManagerProxy.getActiveCall()); 179 mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL, builder.build()); 180 } 181 hideIncomingCallNotification()182 private void hideIncomingCallNotification() { 183 Log.i(this, "hideIncomingCallNotification"); 184 mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL); 185 } 186 getNotificationName(Call call)187 private String getNotificationName(Call call) { 188 String name = ""; 189 if (call.getCallerDisplayNamePresentation() == TelecomManager.PRESENTATION_ALLOWED) { 190 name = call.getCallerDisplayName(); 191 } 192 if (TextUtils.isEmpty(name)) { 193 name = call.getName(); 194 } 195 196 if (TextUtils.isEmpty(name)) { 197 name = call.getPhoneNumber(); 198 } 199 return name; 200 } 201 getNotificationBuilder(Call incomingCall, Call ongoingCall)202 private Notification.Builder getNotificationBuilder(Call incomingCall, Call ongoingCall) { 203 // Change the notification app name to "Android System" to sufficiently distinguish this 204 // from the phone app's name. 205 Bundle extras = new Bundle(); 206 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, mContext.getString( 207 com.android.internal.R.string.android_system_label)); 208 209 Intent answerIntent = new Intent( 210 TelecomBroadcastIntentProcessor.ACTION_ANSWER_FROM_NOTIFICATION, null, mContext, 211 TelecomBroadcastReceiver.class); 212 Intent rejectIntent = new Intent( 213 TelecomBroadcastIntentProcessor.ACTION_REJECT_FROM_NOTIFICATION, null, mContext, 214 TelecomBroadcastReceiver.class); 215 216 String nameOrNumber = getNotificationName(incomingCall); 217 CharSequence viaApp = incomingCall.getTargetPhoneAccountLabel(); 218 boolean isIncomingVideo = VideoProfile.isVideo(incomingCall.getVideoState()); 219 boolean isOngoingVideo = ongoingCall != null ? 220 VideoProfile.isVideo(ongoingCall.getVideoState()) : false; 221 int numOtherCalls = ongoingCall != null ? 222 mCallsManagerProxy.getNumUnholdableCallsForOtherConnectionService( 223 incomingCall.getTargetPhoneAccount()) : 1; 224 225 // Build the "IncomingApp call from John Smith" message. 226 CharSequence incomingCallText; 227 if (isIncomingVideo) { 228 incomingCallText = mContext.getString(R.string.notification_incoming_video_call, viaApp, 229 nameOrNumber); 230 } else { 231 incomingCallText = mContext.getString(R.string.notification_incoming_call, viaApp, 232 nameOrNumber); 233 } 234 235 // Build the "Answering will end your OtherApp call" line. 236 CharSequence disconnectText; 237 if (ongoingCall != null && ongoingCall.isSelfManaged()) { 238 CharSequence ongoingApp = ongoingCall.getTargetPhoneAccountLabel(); 239 // For an ongoing self-managed call, we use a message like: 240 // "Answering will end your OtherApp call". 241 if (numOtherCalls > 1) { 242 // Multiple ongoing calls in the other app, so don't bother specifing whether it is 243 // a video call or audio call. 244 disconnectText = mContext.getString(R.string.answering_ends_other_calls, 245 ongoingApp); 246 } else if (isOngoingVideo) { 247 disconnectText = mContext.getString(R.string.answering_ends_other_video_call, 248 ongoingApp); 249 } else { 250 disconnectText = mContext.getString(R.string.answering_ends_other_call, ongoingApp); 251 } 252 } else { 253 // For an ongoing managed call, we use a message like: 254 // "Answering will end your ongoing call". 255 if (numOtherCalls > 1) { 256 // Multiple ongoing manage calls, so don't bother specifing whether it is a video 257 // call or audio call. 258 disconnectText = mContext.getString(R.string.answering_ends_other_managed_calls); 259 } else if (isOngoingVideo) { 260 disconnectText = mContext.getString( 261 R.string.answering_ends_other_managed_video_call); 262 } else { 263 disconnectText = mContext.getString(R.string.answering_ends_other_managed_call); 264 } 265 } 266 267 final Notification.Builder builder = new Notification.Builder(mContext); 268 builder.setOngoing(true); 269 builder.setExtras(extras); 270 builder.setPriority(Notification.PRIORITY_HIGH); 271 builder.setCategory(Notification.CATEGORY_CALL); 272 builder.setContentTitle(incomingCallText); 273 builder.setContentText(disconnectText); 274 builder.setSmallIcon(R.drawable.ic_phone); 275 builder.setChannelId(NotificationChannelManager.CHANNEL_ID_INCOMING_CALLS); 276 // Ensures this is a heads up notification. A heads-up notification is typically only shown 277 // if there is a fullscreen intent. However since this notification doesn't have that we 278 // will use this trick to get it to show as one anyways. 279 builder.setVibrate(new long[0]); 280 builder.setColor(mContext.getResources().getColor(R.color.theme_color)); 281 builder.addAction( 282 R.anim.on_going_call, 283 getActionText(R.string.answer_incoming_call, R.color.notification_action_answer), 284 PendingIntent.getBroadcast(mContext, 0, answerIntent, 285 PendingIntent.FLAG_CANCEL_CURRENT)); 286 builder.addAction( 287 R.drawable.ic_close_dk, 288 getActionText(R.string.decline_incoming_call, R.color.notification_action_decline), 289 PendingIntent.getBroadcast(mContext, 0, rejectIntent, 290 PendingIntent.FLAG_CANCEL_CURRENT)); 291 return builder; 292 } 293 getActionText(int stringRes, int colorRes)294 private CharSequence getActionText(int stringRes, int colorRes) { 295 CharSequence string = mContext.getText(stringRes); 296 if (string == null) { 297 return ""; 298 } 299 Spannable spannable = new SpannableString(string); 300 spannable.setSpan( 301 new ForegroundColorSpan(mContext.getColor(colorRes)), 0, spannable.length(), 0); 302 return spannable; 303 } 304 } 305