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