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