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