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 {
make(Context context, CallsManagerProxy mCallsManagerProxy)54         IncomingCallNotifier make(Context context, CallsManagerProxy mCallsManagerProxy);
55     }
56 
57     /**
58      * Eliminates strict dependency between this class and CallsManager.
59      */
60     public interface CallsManagerProxy {
hasUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle)61         boolean hasUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle);
getNumUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle)62         int getNumUnholdableCallsForOtherConnectionService(PhoneAccountHandle phoneAccountHandle);
getActiveCall()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 
IncomingCallNotifier(Context context)91     public IncomingCallNotifier(Context context) {
92         mContext = context;
93         mNotificationManager =
94                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
95     }
96 
setCallsManagerProxy(CallsManagerProxy callsManagerProxy)97     public void setCallsManagerProxy(CallsManagerProxy callsManagerProxy) {
98         mCallsManagerProxy = callsManagerProxy;
99     }
100 
getIncomingCall()101     public Call getIncomingCall() {
102         return mIncomingCall;
103     }
104 
105     @Override
onCallAdded(Call call)106     public void onCallAdded(Call call) {
107         if (!mCalls.contains(call)) {
108             mCalls.add(call);
109         }
110 
111         updateIncomingCall();
112     }
113 
114     @Override
onCallRemoved(Call call)115     public void onCallRemoved(Call call) {
116         if (mCalls.contains(call)) {
117             mCalls.remove(call);
118         }
119 
120         updateIncomingCall();
121     }
122 
123     @Override
onCallStateChanged(Call call, int oldState, int newState)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      */
updateIncomingCall()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.hasUnholdableCallsForOtherConnectionService(
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.getNumUnholdableCallsForOtherConnectionService(
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