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.PhoneAccount; 27 import android.telecom.PhoneAccountHandle; 28 import android.telecom.TelecomManager; 29 import android.telecom.VideoProfile; 30 import android.text.Spannable; 31 import android.text.SpannableString; 32 import android.text.TextUtils; 33 import android.text.style.ForegroundColorSpan; 34 import android.util.ArraySet; 35 36 import com.android.internal.annotations.VisibleForTesting; 37 import com.android.internal.telephony.CallerInfo; 38 import com.android.server.telecom.Call; 39 import com.android.server.telecom.CallState; 40 import com.android.server.telecom.CallsManagerListenerBase; 41 import com.android.server.telecom.R; 42 import com.android.server.telecom.TelecomBroadcastIntentProcessor; 43 import com.android.server.telecom.components.TelecomBroadcastReceiver; 44 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 { hasCallsForOtherPhoneAccount(PhoneAccountHandle phoneAccountHandle)62 boolean hasCallsForOtherPhoneAccount(PhoneAccountHandle phoneAccountHandle); getNumCallsForOtherPhoneAccount(PhoneAccountHandle phoneAccountHandle)63 int getNumCallsForOtherPhoneAccount(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 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 if (!mCalls.contains(call)) { 109 mCalls.add(call); 110 } 111 112 updateIncomingCall(); 113 } 114 115 @Override onCallRemoved(Call call)116 public void onCallRemoved(Call call) { 117 if (mCalls.contains(call)) { 118 mCalls.remove(call); 119 } 120 121 updateIncomingCall(); 122 } 123 124 @Override onCallStateChanged(Call call, int oldState, int newState)125 public void onCallStateChanged(Call call, int oldState, int newState) { 126 updateIncomingCall(); 127 } 128 129 /** 130 * Determines which call is the active ringing call at this time and triggers the display of the 131 * UI. 132 */ updateIncomingCall()133 private void updateIncomingCall() { 134 Optional<Call> incomingCallOp = mCalls.stream() 135 .filter(call -> call.isSelfManaged() && call.isIncoming() && 136 call.getState() == CallState.RINGING) 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 showIncomingCallNotification(Call call)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 hideIncomingCallNotification()173 private void hideIncomingCallNotification() { 174 Log.i(this, "hideIncomingCallNotification"); 175 mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL); 176 } 177 getNotificationName(Call call)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 getNotificationBuilder(Call incomingCall, Call ongoingCall)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 getActionText(int stringRes, int colorRes)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