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