1 /*
2  * Copyright (C) 2013 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.incallui;
18 
19 import static com.android.contacts.common.compat.CallSdkCompat.Details.PROPERTY_ENTERPRISE_CALL;
20 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST;
21 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL;
22 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL;
23 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL;
24 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
25 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
26 
27 import com.google.common.base.Preconditions;
28 
29 import android.app.Notification;
30 import android.app.NotificationManager;
31 import android.app.PendingIntent;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.graphics.Bitmap;
35 import android.graphics.BitmapFactory;
36 import android.graphics.drawable.BitmapDrawable;
37 import android.media.AudioAttributes;
38 import android.net.Uri;
39 import android.provider.ContactsContract.Contacts;
40 import android.support.annotation.Nullable;
41 import android.telecom.Call.Details;
42 import android.telecom.PhoneAccount;
43 import android.telecom.TelecomManager;
44 import android.text.BidiFormatter;
45 import android.text.TextDirectionHeuristics;
46 import android.text.TextUtils;
47 
48 import com.android.contacts.common.ContactsUtils;
49 import com.android.contacts.common.ContactsUtils.UserType;
50 import com.android.contacts.common.preference.ContactsPreferences;
51 import com.android.contacts.common.testing.NeededForTesting;
52 import com.android.contacts.common.util.BitmapUtil;
53 import com.android.contacts.common.util.ContactDisplayUtils;
54 import com.android.dialer.R;
55 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
56 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
57 import com.android.incallui.InCallPresenter.InCallState;
58 import com.android.incallui.async.PausableExecutorImpl;
59 import com.android.incallui.ringtone.DialerRingtoneManager;
60 import com.android.incallui.ringtone.InCallTonePlayer;
61 import com.android.incallui.ringtone.ToneGeneratorFactory;
62 
63 import java.util.Objects;
64 
65 /**
66  * This class adds Notifications to the status bar for the in-call experience.
67  */
68 public class StatusBarNotifier implements InCallPresenter.InCallStateListener,
69         CallList.CallUpdateListener {
70 
71     // Notification types
72     // Indicates that no notification is currently showing.
73     private static final int NOTIFICATION_NONE = 0;
74     // Notification for an active call. This is non-interruptive, but cannot be dismissed.
75     private static final int NOTIFICATION_IN_CALL = 1;
76     // Notification for incoming calls. This is interruptive and will show up as a HUN.
77     private static final int NOTIFICATION_INCOMING_CALL = 2;
78 
79     private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000};
80 
81     private final Context mContext;
82     @Nullable private ContactsPreferences mContactsPreferences;
83     private final ContactInfoCache mContactInfoCache;
84     private final NotificationManager mNotificationManager;
85     private final DialerRingtoneManager mDialerRingtoneManager;
86     private int mCurrentNotification = NOTIFICATION_NONE;
87     private int mCallState = Call.State.INVALID;
88     private int mSavedIcon = 0;
89     private String mSavedContent = null;
90     private Bitmap mSavedLargeIcon;
91     private String mSavedContentTitle;
92     private String mCallId = null;
93     private InCallState mInCallState;
94     private Uri mRingtone;
95 
StatusBarNotifier(Context context, ContactInfoCache contactInfoCache)96     public StatusBarNotifier(Context context, ContactInfoCache contactInfoCache) {
97         Preconditions.checkNotNull(context);
98         mContext = context;
99         mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
100         mContactInfoCache = contactInfoCache;
101         mNotificationManager =
102                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
103         mDialerRingtoneManager = new DialerRingtoneManager(
104                 new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()),
105                 CallList.getInstance());
106         mCurrentNotification = NOTIFICATION_NONE;
107     }
108 
109     /**
110      * Creates notifications according to the state we receive from {@link InCallPresenter}.
111      */
112     @Override
onStateChange(InCallState oldState, InCallState newState, CallList callList)113     public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
114         Log.d(this, "onStateChange");
115         mInCallState = newState;
116         updateNotification(newState, callList);
117     }
118 
119     /**
120      * Updates the phone app's status bar notification *and* launches the
121      * incoming call UI in response to a new incoming call.
122      *
123      * If an incoming call is ringing (or call-waiting), the notification
124      * will also include a "fullScreenIntent" that will cause the
125      * InCallScreen to be launched, unless the current foreground activity
126      * is marked as "immersive".
127      *
128      * (This is the mechanism that actually brings up the incoming call UI
129      * when we receive a "new ringing connection" event from the telephony
130      * layer.)
131      *
132      * Also note that this method is safe to call even if the phone isn't
133      * actually ringing (or, more likely, if an incoming call *was*
134      * ringing briefly but then disconnected).  In that case, we'll simply
135      * update or cancel the in-call notification based on the current
136      * phone state.
137      *
138      * @see #updateInCallNotification(InCallState,CallList)
139      */
updateNotification(InCallState state, CallList callList)140     public void updateNotification(InCallState state, CallList callList) {
141         updateInCallNotification(state, callList);
142     }
143 
144     /**
145      * Take down the in-call notification.
146      * @see #updateInCallNotification(InCallState,CallList)
147      */
cancelNotification()148     private void cancelNotification() {
149         if (!TextUtils.isEmpty(mCallId)) {
150             CallList.getInstance().removeCallUpdateListener(mCallId, this);
151             mCallId = null;
152         }
153         if (mCurrentNotification != NOTIFICATION_NONE) {
154             Log.d(this, "cancelInCall()...");
155             mNotificationManager.cancel(mCurrentNotification);
156         }
157         mCurrentNotification = NOTIFICATION_NONE;
158     }
159 
160     /**
161      * Should only be called from a irrecoverable state where it is necessary to dismiss all
162      * notifications.
163      */
clearAllCallNotifications(Context backupContext)164     static void clearAllCallNotifications(Context backupContext) {
165         Log.i(StatusBarNotifier.class.getSimpleName(),
166                 "Something terrible happened. Clear all InCall notifications");
167 
168         NotificationManager notificationManager =
169                 (NotificationManager) backupContext.getSystemService(Context.NOTIFICATION_SERVICE);
170         notificationManager.cancel(NOTIFICATION_IN_CALL);
171         notificationManager.cancel(NOTIFICATION_INCOMING_CALL);
172     }
173 
174     /**
175      * Helper method for updateInCallNotification() and
176      * updateNotification(): Update the phone app's
177      * status bar notification based on the current telephony state, or
178      * cancels the notification if the phone is totally idle.
179      */
updateInCallNotification(final InCallState state, CallList callList)180     private void updateInCallNotification(final InCallState state, CallList callList) {
181         Log.d(this, "updateInCallNotification...");
182 
183         final Call call = getCallToShow(callList);
184 
185         if (call != null) {
186             showNotification(call);
187         } else {
188             cancelNotification();
189         }
190     }
191 
showNotification(final Call call)192     private void showNotification(final Call call) {
193         final boolean isIncoming = (call.getState() == Call.State.INCOMING ||
194                 call.getState() == Call.State.CALL_WAITING);
195         if (!TextUtils.isEmpty(mCallId)) {
196             CallList.getInstance().removeCallUpdateListener(mCallId, this);
197         }
198         mCallId = call.getId();
199         CallList.getInstance().addCallUpdateListener(call.getId(), this);
200 
201         // we make a call to the contact info cache to query for supplemental data to what the
202         // call provides.  This includes the contact name and photo.
203         // This callback will always get called immediately and synchronously with whatever data
204         // it has available, and may make a subsequent call later (same thread) if it had to
205         // call into the contacts provider for more data.
206         mContactInfoCache.findInfo(call, isIncoming, new ContactInfoCacheCallback() {
207             @Override
208             public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
209                 Call call = CallList.getInstance().getCallById(callId);
210                 if (call != null) {
211                     call.getLogState().contactLookupResult = entry.contactLookupResult;
212                     buildAndSendNotification(call, entry);
213                 }
214             }
215 
216             @Override
217             public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
218                 Call call = CallList.getInstance().getCallById(callId);
219                 if (call != null) {
220                     buildAndSendNotification(call, entry);
221                 }
222             }
223 
224             @Override
225             public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {}
226         });
227     }
228 
229     /**
230      * Sets up the main Ui for the notification
231      */
buildAndSendNotification(Call originalCall, ContactCacheEntry contactInfo)232     private void buildAndSendNotification(Call originalCall, ContactCacheEntry contactInfo) {
233         // This can get called to update an existing notification after contact information has come
234         // back. However, it can happen much later. Before we continue, we need to make sure that
235         // the call being passed in is still the one we want to show in the notification.
236         final Call call = getCallToShow(CallList.getInstance());
237         if (call == null || !call.getId().equals(originalCall.getId())) {
238             return;
239         }
240 
241         final int callState = call.getState();
242 
243         // Check if data has changed; if nothing is different, don't issue another notification.
244         final int iconResId = getIconToDisplay(call);
245         Bitmap largeIcon = getLargeIconToDisplay(contactInfo, call);
246         final String content =
247                 getContentString(call, contactInfo.userType);
248         final String contentTitle = getContentTitle(contactInfo, call);
249 
250         final int notificationType;
251         if (callState == Call.State.INCOMING || callState == Call.State.CALL_WAITING) {
252             notificationType = NOTIFICATION_INCOMING_CALL;
253         } else {
254             notificationType = NOTIFICATION_IN_CALL;
255         }
256 
257         if (!checkForChangeAndSaveData(iconResId, content, largeIcon, contentTitle, callState,
258                 notificationType, contactInfo.contactRingtoneUri)) {
259             return;
260         }
261 
262         if (largeIcon != null) {
263             largeIcon = getRoundedIcon(largeIcon);
264         }
265 
266         /*
267          * This builder is used for the notification shown when the device is locked and the user
268          * has set their notification settings to 'hide sensitive content'
269          * {@see Notification.Builder#setPublicVersion}.
270          */
271         Notification.Builder publicBuilder = new Notification.Builder(mContext);
272         publicBuilder.setSmallIcon(iconResId)
273                 .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
274                 // Hide work call state for the lock screen notification
275                 .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
276         setNotificationWhen(call, callState, publicBuilder);
277 
278         /*
279          * Builder for the notification shown when the device is unlocked or the user has set their
280          * notification settings to 'show all notification content'.
281          */
282         final Notification.Builder builder = getNotificationBuilder();
283         builder.setPublicVersion(publicBuilder.build());
284 
285         // Set up the main intent to send the user to the in-call screen
286         final PendingIntent inCallPendingIntent = createLaunchPendingIntent();
287         builder.setContentIntent(inCallPendingIntent);
288 
289         // Set the intent as a full screen intent as well if a call is incoming
290         if (notificationType == NOTIFICATION_INCOMING_CALL
291                 && !InCallPresenter.getInstance().isShowingInCallUi()) {
292             configureFullScreenIntent(builder, inCallPendingIntent, call);
293             // Set the notification category for incoming calls
294             builder.setCategory(Notification.CATEGORY_CALL);
295         }
296 
297         // Set the content
298         builder.setContentText(content);
299         builder.setSmallIcon(iconResId);
300         builder.setContentTitle(contentTitle);
301         builder.setLargeIcon(largeIcon);
302         builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
303 
304         final boolean isVideoUpgradeRequest = call.getSessionModificationState()
305                 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
306         if (isVideoUpgradeRequest) {
307             builder.setUsesChronometer(false);
308             addDismissUpgradeRequestAction(builder);
309             addAcceptUpgradeRequestAction(builder);
310         } else {
311             createIncomingCallNotification(call, callState, builder);
312         }
313 
314         addPersonReference(builder, contactInfo, call);
315 
316         /*
317          * Fire off the notification
318          */
319         Notification notification = builder.build();
320 
321         if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) {
322             notification.flags |= Notification.FLAG_INSISTENT;
323             notification.sound = contactInfo.contactRingtoneUri;
324             AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder();
325             audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
326             audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
327             notification.audioAttributes = audioAttributes.build();
328             if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) {
329                 notification.vibrate = VIBRATE_PATTERN;
330             }
331         }
332         if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
333             Log.v(this, "Playing call waiting tone");
334             mDialerRingtoneManager.playCallWaitingTone();
335         }
336         if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
337             Log.i(this, "Previous notification already showing - cancelling "
338                     + mCurrentNotification);
339             mNotificationManager.cancel(mCurrentNotification);
340         }
341         Log.i(this, "Displaying notification for " + notificationType);
342         mNotificationManager.notify(notificationType, notification);
343         mCurrentNotification = notificationType;
344     }
345 
createIncomingCallNotification( Call call, int state, Notification.Builder builder)346     private void createIncomingCallNotification(
347             Call call, int state, Notification.Builder builder) {
348         setNotificationWhen(call, state, builder);
349 
350         // Add hang up option for any active calls (active | onhold), outgoing calls (dialing).
351         if (state == Call.State.ACTIVE ||
352                 state == Call.State.ONHOLD ||
353                 Call.State.isDialing(state)) {
354             addHangupAction(builder);
355         } else if (state == Call.State.INCOMING || state == Call.State.CALL_WAITING) {
356             addDismissAction(builder);
357             if (call.isVideoCall(mContext)) {
358                 addVoiceAction(builder);
359                 addVideoCallAction(builder);
360             } else {
361                 addAnswerAction(builder);
362             }
363         }
364     }
365 
366     /*
367      * Sets the notification's when section as needed. For active calls, this is explicitly set as
368      * the duration of the call. For all other states, the notification will automatically show the
369      * time at which the notification was created.
370      */
setNotificationWhen(Call call, int state, Notification.Builder builder)371     private void setNotificationWhen(Call call, int state, Notification.Builder builder) {
372         if (state == Call.State.ACTIVE) {
373             builder.setUsesChronometer(true);
374             builder.setWhen(call.getConnectTimeMillis());
375         } else {
376             builder.setUsesChronometer(false);
377         }
378     }
379 
380     /**
381      * Checks the new notification data and compares it against any notification that we
382      * are already displaying. If the data is exactly the same, we return false so that
383      * we do not issue a new notification for the exact same data.
384      */
checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon, String contentTitle, int state, int notificationType, Uri ringtone)385     private boolean checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon,
386             String contentTitle, int state, int notificationType, Uri ringtone) {
387 
388         // The two are different:
389         // if new title is not null, it should be different from saved version OR
390         // if new title is null, the saved version should not be null
391         final boolean contentTitleChanged =
392                 (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) ||
393                 (contentTitle == null && mSavedContentTitle != null);
394 
395         // any change means we are definitely updating
396         boolean retval = (mSavedIcon != icon) || !Objects.equals(mSavedContent, content)
397                 || (mCallState != state) || (mSavedLargeIcon != largeIcon)
398                 || contentTitleChanged || !Objects.equals(mRingtone, ringtone);
399 
400         // If we aren't showing a notification right now or the notification type is changing,
401         // definitely do an update.
402         if (mCurrentNotification != notificationType) {
403             if (mCurrentNotification == NOTIFICATION_NONE) {
404                 Log.d(this, "Showing notification for first time.");
405             }
406             retval = true;
407         }
408 
409         mSavedIcon = icon;
410         mSavedContent = content;
411         mCallState = state;
412         mSavedLargeIcon = largeIcon;
413         mSavedContentTitle = contentTitle;
414         mRingtone = ringtone;
415 
416         if (retval) {
417             Log.d(this, "Data changed.  Showing notification");
418         }
419 
420         return retval;
421     }
422 
423     /**
424      * Returns the main string to use in the notification.
425      */
426     @NeededForTesting
getContentTitle(ContactCacheEntry contactInfo, Call call)427     String getContentTitle(ContactCacheEntry contactInfo, Call call) {
428         if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
429             return mContext.getResources().getString(R.string.card_title_conf_call);
430         }
431 
432         String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary,
433                     contactInfo.nameAlternative, mContactsPreferences);
434         if (TextUtils.isEmpty(preferredName)) {
435             return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance()
436                     .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
437         }
438         return preferredName;
439     }
440 
addPersonReference(Notification.Builder builder, ContactCacheEntry contactInfo, Call call)441     private void addPersonReference(Notification.Builder builder, ContactCacheEntry contactInfo,
442             Call call) {
443         // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
444         // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
445         // NotificationManager using it.
446         if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
447             builder.addPerson(contactInfo.lookupUri.toString());
448         } else if (!TextUtils.isEmpty(call.getNumber())) {
449             builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL,
450                     call.getNumber(), null).toString());
451         }
452     }
453 
454     /**
455      * Gets a large icon from the contact info object to display in the notification.
456      */
getLargeIconToDisplay(ContactCacheEntry contactInfo, Call call)457     private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, Call call) {
458         Bitmap largeIcon = null;
459         if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
460             largeIcon = BitmapFactory.decodeResource(mContext.getResources(),
461                     R.drawable.img_conference);
462         }
463         if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
464             largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
465         }
466         return largeIcon;
467     }
468 
getRoundedIcon(Bitmap bitmap)469     private Bitmap getRoundedIcon(Bitmap bitmap) {
470         if (bitmap == null) {
471             return null;
472         }
473         final int height = (int) mContext.getResources().getDimension(
474                 android.R.dimen.notification_large_icon_height);
475         final int width = (int) mContext.getResources().getDimension(
476                 android.R.dimen.notification_large_icon_width);
477         return BitmapUtil.getRoundedBitmap(bitmap, width, height);
478     }
479 
480     /**
481      * Returns the appropriate icon res Id to display based on the call for which
482      * we want to display information.
483      */
getIconToDisplay(Call call)484     private int getIconToDisplay(Call call) {
485         // Even if both lines are in use, we only show a single item in
486         // the expanded Notifications UI.  It's labeled "Ongoing call"
487         // (or "On hold" if there's only one call, and it's on hold.)
488         // Also, we don't have room to display caller-id info from two
489         // different calls.  So if both lines are in use, display info
490         // from the foreground call.  And if there's a ringing call,
491         // display that regardless of the state of the other calls.
492         if (call.getState() == Call.State.ONHOLD) {
493             return R.drawable.ic_phone_paused_white_24dp;
494         } else if (call.getSessionModificationState()
495                 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
496             return R.drawable.ic_videocam;
497         }
498         return R.drawable.ic_call_white_24dp;
499     }
500 
501     /**
502      * Returns the message to use with the notification.
503      */
getContentString(Call call, @UserType long userType)504     private String getContentString(Call call, @UserType long userType) {
505         boolean isIncomingOrWaiting = call.getState() == Call.State.INCOMING ||
506                 call.getState() == Call.State.CALL_WAITING;
507 
508         if (isIncomingOrWaiting &&
509                 call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) {
510 
511             if (!TextUtils.isEmpty(call.getChildNumber())) {
512                 return mContext.getString(R.string.child_number, call.getChildNumber());
513             } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) {
514                 return call.getCallSubject();
515             }
516         }
517 
518         int resId = R.string.notification_ongoing_call;
519         if (call.hasProperty(Details.PROPERTY_WIFI)) {
520             resId = R.string.notification_ongoing_call_wifi;
521         }
522 
523         if (isIncomingOrWaiting) {
524             if (call.hasProperty(Details.PROPERTY_WIFI)) {
525                 resId = R.string.notification_incoming_call_wifi;
526             } else {
527                 resId = R.string.notification_incoming_call;
528             }
529         } else if (call.getState() == Call.State.ONHOLD) {
530             resId = R.string.notification_on_hold;
531         } else if (Call.State.isDialing(call.getState())) {
532             resId = R.string.notification_dialing;
533         } else if (call.getSessionModificationState()
534                 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
535             resId = R.string.notification_requesting_video_call;
536         }
537 
538         // Is the call placed through work connection service.
539         boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL);
540         if(userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) {
541             resId = getWorkStringFromPersonalString(resId);
542         }
543 
544         return mContext.getString(resId);
545     }
546 
getWorkStringFromPersonalString(int resId)547     private static int getWorkStringFromPersonalString(int resId) {
548         if (resId == R.string.notification_ongoing_call) {
549             return R.string.notification_ongoing_work_call;
550         } else if (resId == R.string.notification_ongoing_call_wifi) {
551             return R.string.notification_ongoing_work_call_wifi;
552         } else if (resId == R.string.notification_incoming_call_wifi) {
553             return R.string.notification_incoming_work_call_wifi;
554         } else if (resId == R.string.notification_incoming_call) {
555             return R.string.notification_incoming_work_call;
556         } else {
557             return resId;
558         }
559     }
560 
561     /**
562      * Gets the most relevant call to display in the notification.
563      */
getCallToShow(CallList callList)564     private Call getCallToShow(CallList callList) {
565         if (callList == null) {
566             return null;
567         }
568         Call call = callList.getIncomingCall();
569         if (call == null) {
570             call = callList.getOutgoingCall();
571         }
572         if (call == null) {
573             call = callList.getVideoUpgradeRequestCall();
574         }
575         if (call == null) {
576             call = callList.getActiveOrBackgroundCall();
577         }
578         return call;
579     }
580 
addAnswerAction(Notification.Builder builder)581     private void addAnswerAction(Notification.Builder builder) {
582         Log.d(this, "Will show \"answer\" action in the incoming call Notification");
583 
584         PendingIntent answerVoicePendingIntent = createNotificationPendingIntent(
585                 mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
586         builder.addAction(R.drawable.ic_call_white_24dp,
587                 mContext.getText(R.string.notification_action_answer),
588                 answerVoicePendingIntent);
589     }
590 
addDismissAction(Notification.Builder builder)591     private void addDismissAction(Notification.Builder builder) {
592         Log.d(this, "Will show \"dismiss\" action in the incoming call Notification");
593 
594         PendingIntent declinePendingIntent =
595                 createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
596         builder.addAction(R.drawable.ic_close_dk,
597                 mContext.getText(R.string.notification_action_dismiss),
598                 declinePendingIntent);
599     }
600 
addHangupAction(Notification.Builder builder)601     private void addHangupAction(Notification.Builder builder) {
602         Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification");
603 
604         PendingIntent hangupPendingIntent =
605                 createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
606         builder.addAction(R.drawable.ic_call_end_white_24dp,
607                 mContext.getText(R.string.notification_action_end_call),
608                 hangupPendingIntent);
609     }
610 
addVideoCallAction(Notification.Builder builder)611     private void addVideoCallAction(Notification.Builder builder) {
612         Log.i(this, "Will show \"video\" action in the incoming call Notification");
613 
614         PendingIntent answerVideoPendingIntent = createNotificationPendingIntent(
615                 mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
616         builder.addAction(R.drawable.ic_videocam,
617                 mContext.getText(R.string.notification_action_answer_video),
618                 answerVideoPendingIntent);
619     }
620 
addVoiceAction(Notification.Builder builder)621     private void addVoiceAction(Notification.Builder builder) {
622         Log.d(this, "Will show \"voice\" action in the incoming call Notification");
623 
624         PendingIntent answerVoicePendingIntent = createNotificationPendingIntent(
625                 mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
626         builder.addAction(R.drawable.ic_call_white_24dp,
627                 mContext.getText(R.string.notification_action_answer_voice),
628                 answerVoicePendingIntent);
629     }
630 
addAcceptUpgradeRequestAction(Notification.Builder builder)631     private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
632         Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification");
633 
634         PendingIntent acceptVideoPendingIntent = createNotificationPendingIntent(
635                 mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
636         builder.addAction(0, mContext.getText(R.string.notification_action_accept),
637                 acceptVideoPendingIntent);
638     }
639 
addDismissUpgradeRequestAction(Notification.Builder builder)640     private void addDismissUpgradeRequestAction(Notification.Builder builder) {
641         Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification");
642 
643         PendingIntent declineVideoPendingIntent = createNotificationPendingIntent(
644                 mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
645         builder.addAction(0, mContext.getText(R.string.notification_action_dismiss),
646                 declineVideoPendingIntent);
647     }
648 
649     /**
650      * Adds fullscreen intent to the builder.
651      */
configureFullScreenIntent(Notification.Builder builder, PendingIntent intent, Call call)652     private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent,
653             Call call) {
654         // Ok, we actually want to launch the incoming call
655         // UI at this point (in addition to simply posting a notification
656         // to the status bar).  Setting fullScreenIntent will cause
657         // the InCallScreen to be launched immediately *unless* the
658         // current foreground activity is marked as "immersive".
659         Log.d(this, "- Setting fullScreenIntent: " + intent);
660         builder.setFullScreenIntent(intent, true);
661 
662         // Ugly hack alert:
663         //
664         // The NotificationManager has the (undocumented) behavior
665         // that it will *ignore* the fullScreenIntent field if you
666         // post a new Notification that matches the ID of one that's
667         // already active.  Unfortunately this is exactly what happens
668         // when you get an incoming call-waiting call:  the
669         // "ongoing call" notification is already visible, so the
670         // InCallScreen won't get launched in this case!
671         // (The result: if you bail out of the in-call UI while on a
672         // call and then get a call-waiting call, the incoming call UI
673         // won't come up automatically.)
674         //
675         // The workaround is to just notice this exact case (this is a
676         // call-waiting call *and* the InCallScreen is not in the
677         // foreground) and manually cancel the in-call notification
678         // before (re)posting it.
679         //
680         // TODO: there should be a cleaner way of avoiding this
681         // problem (see discussion in bug 3184149.)
682 
683         // If a call is onhold during an incoming call, the call actually comes in as
684         // INCOMING.  For that case *and* traditional call-waiting, we want to
685         // cancel the notification.
686         boolean isCallWaiting = (call.getState() == Call.State.CALL_WAITING ||
687                 (call.getState() == Call.State.INCOMING &&
688                         CallList.getInstance().getBackgroundCall() != null));
689 
690         if (isCallWaiting) {
691             Log.i(this, "updateInCallNotification: call-waiting! force relaunch...");
692             // Cancel the IN_CALL_NOTIFICATION immediately before
693             // (re)posting it; this seems to force the
694             // NotificationManager to launch the fullScreenIntent.
695             mNotificationManager.cancel(NOTIFICATION_IN_CALL);
696         }
697     }
698 
getNotificationBuilder()699     private Notification.Builder getNotificationBuilder() {
700         final Notification.Builder builder = new Notification.Builder(mContext);
701         builder.setOngoing(true);
702 
703         // Make the notification prioritized over the other normal notifications.
704         builder.setPriority(Notification.PRIORITY_HIGH);
705 
706         return builder;
707     }
708 
createLaunchPendingIntent()709     private PendingIntent createLaunchPendingIntent() {
710 
711         final Intent intent = InCallPresenter.getInstance().getInCallIntent(
712                 false /* showDialpad */, false /* newOutgoingCall */);
713 
714         // PendingIntent that can be used to launch the InCallActivity.  The
715         // system fires off this intent if the user pulls down the windowshade
716         // and clicks the notification's expanded view.  It's also used to
717         // launch the InCallActivity immediately when when there's an incoming
718         // call (see the "fullScreenIntent" field below).
719         PendingIntent inCallPendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
720 
721         return inCallPendingIntent;
722     }
723 
724     /**
725      * Returns PendingIntent for answering a phone call. This will typically be used from
726      * Notification context.
727      */
createNotificationPendingIntent(Context context, String action)728     private static PendingIntent createNotificationPendingIntent(Context context, String action) {
729         final Intent intent = new Intent(action, null,
730                 context, NotificationBroadcastReceiver.class);
731         return PendingIntent.getBroadcast(context, 0, intent, 0);
732     }
733 
734     @Override
onCallChanged(Call call)735     public void onCallChanged(Call call) {
736         if (CallList.getInstance().getIncomingCall() == null) {
737             mDialerRingtoneManager.stopCallWaitingTone();
738         }
739     }
740 
741     /**
742      * Responds to changes in the session modification state for the call by dismissing the
743      * status bar notification as required.
744      *
745      * @param sessionModificationState The new session modification state.
746      */
747     @Override
onSessionModificationStateChange(int sessionModificationState)748     public void onSessionModificationStateChange(int sessionModificationState) {
749         if (sessionModificationState == Call.SessionModificationState.NO_REQUEST) {
750             if (mCallId != null) {
751                 CallList.getInstance().removeCallUpdateListener(mCallId, this);
752             }
753 
754             updateNotification(mInCallState, CallList.getInstance());
755         }
756     }
757 
758     @Override
onLastForwardedNumberChange()759     public void onLastForwardedNumberChange() {
760         // no-op
761     }
762 
763     @Override
onChildNumberChange()764     public void onChildNumberChange() {
765         // no-op
766     }
767 }
768