• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 boolean isVideoUpgradeRequest = call.getSessionModificationState()
251                 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
252         final int notificationType;
253         if (callState == Call.State.INCOMING || callState == Call.State.CALL_WAITING
254                 || isVideoUpgradeRequest) {
255             notificationType = NOTIFICATION_INCOMING_CALL;
256         } else {
257             notificationType = NOTIFICATION_IN_CALL;
258         }
259 
260         if (!checkForChangeAndSaveData(iconResId, content, largeIcon, contentTitle, callState,
261                 notificationType, contactInfo.contactRingtoneUri)) {
262             return;
263         }
264 
265         if (largeIcon != null) {
266             largeIcon = getRoundedIcon(largeIcon);
267         }
268 
269         /*
270          * This builder is used for the notification shown when the device is locked and the user
271          * has set their notification settings to 'hide sensitive content'
272          * {@see Notification.Builder#setPublicVersion}.
273          */
274         Notification.Builder publicBuilder = new Notification.Builder(mContext);
275         publicBuilder.setSmallIcon(iconResId)
276                 .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
277                 // Hide work call state for the lock screen notification
278                 .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
279         setNotificationWhen(call, callState, publicBuilder);
280 
281         /*
282          * Builder for the notification shown when the device is unlocked or the user has set their
283          * notification settings to 'show all notification content'.
284          */
285         final Notification.Builder builder = getNotificationBuilder();
286         builder.setPublicVersion(publicBuilder.build());
287 
288         // Set up the main intent to send the user to the in-call screen
289         final PendingIntent inCallPendingIntent = createLaunchPendingIntent();
290         builder.setContentIntent(inCallPendingIntent);
291 
292         // Set the intent as a full screen intent as well if a call is incoming
293         if (notificationType == NOTIFICATION_INCOMING_CALL
294                 && !InCallPresenter.getInstance().isShowingInCallUi()) {
295             configureFullScreenIntent(builder, inCallPendingIntent, call);
296             // Set the notification category for incoming calls
297             builder.setCategory(Notification.CATEGORY_CALL);
298         }
299 
300         // Set the content
301         builder.setContentText(content);
302         builder.setSmallIcon(iconResId);
303         builder.setContentTitle(contentTitle);
304         builder.setLargeIcon(largeIcon);
305         builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
306 
307         if (isVideoUpgradeRequest) {
308             builder.setUsesChronometer(false);
309             addDismissUpgradeRequestAction(builder);
310             addAcceptUpgradeRequestAction(builder);
311         } else {
312             createIncomingCallNotification(call, callState, builder);
313         }
314 
315         addPersonReference(builder, contactInfo, call);
316 
317         /*
318          * Fire off the notification
319          */
320         Notification notification = builder.build();
321 
322         if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) {
323             notification.flags |= Notification.FLAG_INSISTENT;
324             notification.sound = contactInfo.contactRingtoneUri;
325             AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder();
326             audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
327             audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
328             notification.audioAttributes = audioAttributes.build();
329             if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) {
330                 notification.vibrate = VIBRATE_PATTERN;
331             }
332         }
333         if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
334             Log.v(this, "Playing call waiting tone");
335             mDialerRingtoneManager.playCallWaitingTone();
336         }
337         if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
338             Log.i(this, "Previous notification already showing - cancelling "
339                     + mCurrentNotification);
340             mNotificationManager.cancel(mCurrentNotification);
341         }
342         Log.i(this, "Displaying notification for " + notificationType);
343         mNotificationManager.notify(notificationType, notification);
344         mCurrentNotification = notificationType;
345     }
346 
createIncomingCallNotification( Call call, int state, Notification.Builder builder)347     private void createIncomingCallNotification(
348             Call call, int state, Notification.Builder builder) {
349         setNotificationWhen(call, state, builder);
350 
351         // Add hang up option for any active calls (active | onhold), outgoing calls (dialing).
352         if (state == Call.State.ACTIVE ||
353                 state == Call.State.ONHOLD ||
354                 Call.State.isDialing(state)) {
355             addHangupAction(builder);
356         } else if (state == Call.State.INCOMING || state == Call.State.CALL_WAITING) {
357             addDismissAction(builder);
358             if (call.isVideoCall(mContext)) {
359                 addVoiceAction(builder);
360                 addVideoCallAction(builder);
361             } else {
362                 addAnswerAction(builder);
363             }
364         }
365     }
366 
367     /*
368      * Sets the notification's when section as needed. For active calls, this is explicitly set as
369      * the duration of the call. For all other states, the notification will automatically show the
370      * time at which the notification was created.
371      */
setNotificationWhen(Call call, int state, Notification.Builder builder)372     private void setNotificationWhen(Call call, int state, Notification.Builder builder) {
373         if (state == Call.State.ACTIVE) {
374             builder.setUsesChronometer(true);
375             builder.setWhen(call.getConnectTimeMillis());
376         } else {
377             builder.setUsesChronometer(false);
378         }
379     }
380 
381     /**
382      * Checks the new notification data and compares it against any notification that we
383      * are already displaying. If the data is exactly the same, we return false so that
384      * we do not issue a new notification for the exact same data.
385      */
checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon, String contentTitle, int state, int notificationType, Uri ringtone)386     private boolean checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon,
387             String contentTitle, int state, int notificationType, Uri ringtone) {
388 
389         // The two are different:
390         // if new title is not null, it should be different from saved version OR
391         // if new title is null, the saved version should not be null
392         final boolean contentTitleChanged =
393                 (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) ||
394                 (contentTitle == null && mSavedContentTitle != null);
395 
396         // any change means we are definitely updating
397         boolean retval = (mSavedIcon != icon) || !Objects.equals(mSavedContent, content)
398                 || (mCallState != state) || (mSavedLargeIcon != largeIcon)
399                 || contentTitleChanged || !Objects.equals(mRingtone, ringtone);
400 
401         // If we aren't showing a notification right now or the notification type is changing,
402         // definitely do an update.
403         if (mCurrentNotification != notificationType) {
404             if (mCurrentNotification == NOTIFICATION_NONE) {
405                 Log.d(this, "Showing notification for first time.");
406             }
407             retval = true;
408         }
409 
410         mSavedIcon = icon;
411         mSavedContent = content;
412         mCallState = state;
413         mSavedLargeIcon = largeIcon;
414         mSavedContentTitle = contentTitle;
415         mRingtone = ringtone;
416 
417         if (retval) {
418             Log.d(this, "Data changed.  Showing notification");
419         }
420 
421         return retval;
422     }
423 
424     /**
425      * Returns the main string to use in the notification.
426      */
427     @NeededForTesting
getContentTitle(ContactCacheEntry contactInfo, Call call)428     String getContentTitle(ContactCacheEntry contactInfo, Call call) {
429         if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
430             return mContext.getResources().getString(R.string.card_title_conf_call);
431         }
432 
433         String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary,
434                     contactInfo.nameAlternative, mContactsPreferences);
435         if (TextUtils.isEmpty(preferredName)) {
436             return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance()
437                     .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
438         }
439         return preferredName;
440     }
441 
addPersonReference(Notification.Builder builder, ContactCacheEntry contactInfo, Call call)442     private void addPersonReference(Notification.Builder builder, ContactCacheEntry contactInfo,
443             Call call) {
444         // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
445         // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
446         // NotificationManager using it.
447         if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
448             builder.addPerson(contactInfo.lookupUri.toString());
449         } else if (!TextUtils.isEmpty(call.getNumber())) {
450             builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL,
451                     call.getNumber(), null).toString());
452         }
453     }
454 
455     /**
456      * Gets a large icon from the contact info object to display in the notification.
457      */
getLargeIconToDisplay(ContactCacheEntry contactInfo, Call call)458     private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, Call call) {
459         Bitmap largeIcon = null;
460         if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
461             largeIcon = BitmapFactory.decodeResource(mContext.getResources(),
462                     R.drawable.img_conference);
463         }
464         if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
465             largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
466         }
467         return largeIcon;
468     }
469 
getRoundedIcon(Bitmap bitmap)470     private Bitmap getRoundedIcon(Bitmap bitmap) {
471         if (bitmap == null) {
472             return null;
473         }
474         final int height = (int) mContext.getResources().getDimension(
475                 android.R.dimen.notification_large_icon_height);
476         final int width = (int) mContext.getResources().getDimension(
477                 android.R.dimen.notification_large_icon_width);
478         return BitmapUtil.getRoundedBitmap(bitmap, width, height);
479     }
480 
481     /**
482      * Returns the appropriate icon res Id to display based on the call for which
483      * we want to display information.
484      */
getIconToDisplay(Call call)485     private int getIconToDisplay(Call call) {
486         // Even if both lines are in use, we only show a single item in
487         // the expanded Notifications UI.  It's labeled "Ongoing call"
488         // (or "On hold" if there's only one call, and it's on hold.)
489         // Also, we don't have room to display caller-id info from two
490         // different calls.  So if both lines are in use, display info
491         // from the foreground call.  And if there's a ringing call,
492         // display that regardless of the state of the other calls.
493         if (call.getState() == Call.State.ONHOLD) {
494             return R.drawable.ic_phone_paused_white_24dp;
495         } else if (call.getSessionModificationState()
496                 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
497             return R.drawable.ic_videocam;
498         }
499         return R.drawable.ic_call_white_24dp;
500     }
501 
502     /**
503      * Returns the message to use with the notification.
504      */
getContentString(Call call, @UserType long userType)505     private String getContentString(Call call, @UserType long userType) {
506         boolean isIncomingOrWaiting = call.getState() == Call.State.INCOMING ||
507                 call.getState() == Call.State.CALL_WAITING;
508 
509         if (isIncomingOrWaiting &&
510                 call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) {
511 
512             if (!TextUtils.isEmpty(call.getChildNumber())) {
513                 return mContext.getString(R.string.child_number, call.getChildNumber());
514             } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) {
515                 return call.getCallSubject();
516             }
517         }
518 
519         int resId = R.string.notification_ongoing_call;
520         if (call.hasProperty(Details.PROPERTY_WIFI)) {
521             resId = R.string.notification_ongoing_call_wifi;
522         }
523 
524         if (isIncomingOrWaiting) {
525             if (call.hasProperty(Details.PROPERTY_WIFI)) {
526                 resId = R.string.notification_incoming_call_wifi;
527             } else {
528                 resId = R.string.notification_incoming_call;
529             }
530         } else if (call.getState() == Call.State.ONHOLD) {
531             resId = R.string.notification_on_hold;
532         } else if (Call.State.isDialing(call.getState())) {
533             resId = R.string.notification_dialing;
534         } else if (call.getSessionModificationState()
535                 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
536             resId = R.string.notification_requesting_video_call;
537         }
538 
539         // Is the call placed through work connection service.
540         boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL);
541         if(userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) {
542             resId = getWorkStringFromPersonalString(resId);
543         }
544 
545         return mContext.getString(resId);
546     }
547 
getWorkStringFromPersonalString(int resId)548     private static int getWorkStringFromPersonalString(int resId) {
549         if (resId == R.string.notification_ongoing_call) {
550             return R.string.notification_ongoing_work_call;
551         } else if (resId == R.string.notification_ongoing_call_wifi) {
552             return R.string.notification_ongoing_work_call_wifi;
553         } else if (resId == R.string.notification_incoming_call_wifi) {
554             return R.string.notification_incoming_work_call_wifi;
555         } else if (resId == R.string.notification_incoming_call) {
556             return R.string.notification_incoming_work_call;
557         } else {
558             return resId;
559         }
560     }
561 
562     /**
563      * Gets the most relevant call to display in the notification.
564      */
getCallToShow(CallList callList)565     private Call getCallToShow(CallList callList) {
566         if (callList == null) {
567             return null;
568         }
569         Call call = callList.getIncomingCall();
570         if (call == null) {
571             call = callList.getOutgoingCall();
572         }
573         if (call == null) {
574             call = callList.getVideoUpgradeRequestCall();
575         }
576         if (call == null) {
577             call = callList.getActiveOrBackgroundCall();
578         }
579         return call;
580     }
581 
addAnswerAction(Notification.Builder builder)582     private void addAnswerAction(Notification.Builder builder) {
583         Log.d(this, "Will show \"answer\" action in the incoming call Notification");
584 
585         PendingIntent answerVoicePendingIntent = createNotificationPendingIntent(
586                 mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
587         builder.addAction(R.drawable.ic_call_white_24dp,
588                 mContext.getText(R.string.notification_action_answer),
589                 answerVoicePendingIntent);
590     }
591 
addDismissAction(Notification.Builder builder)592     private void addDismissAction(Notification.Builder builder) {
593         Log.d(this, "Will show \"dismiss\" action in the incoming call Notification");
594 
595         PendingIntent declinePendingIntent =
596                 createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
597         builder.addAction(R.drawable.ic_close_dk,
598                 mContext.getText(R.string.notification_action_dismiss),
599                 declinePendingIntent);
600     }
601 
addHangupAction(Notification.Builder builder)602     private void addHangupAction(Notification.Builder builder) {
603         Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification");
604 
605         PendingIntent hangupPendingIntent =
606                 createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
607         builder.addAction(R.drawable.ic_call_end_white_24dp,
608                 mContext.getText(R.string.notification_action_end_call),
609                 hangupPendingIntent);
610     }
611 
addVideoCallAction(Notification.Builder builder)612     private void addVideoCallAction(Notification.Builder builder) {
613         Log.i(this, "Will show \"video\" action in the incoming call Notification");
614 
615         PendingIntent answerVideoPendingIntent = createNotificationPendingIntent(
616                 mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
617         builder.addAction(R.drawable.ic_videocam,
618                 mContext.getText(R.string.notification_action_answer_video),
619                 answerVideoPendingIntent);
620     }
621 
addVoiceAction(Notification.Builder builder)622     private void addVoiceAction(Notification.Builder builder) {
623         Log.d(this, "Will show \"voice\" action in the incoming call Notification");
624 
625         PendingIntent answerVoicePendingIntent = createNotificationPendingIntent(
626                 mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
627         builder.addAction(R.drawable.ic_call_white_24dp,
628                 mContext.getText(R.string.notification_action_answer_voice),
629                 answerVoicePendingIntent);
630     }
631 
addAcceptUpgradeRequestAction(Notification.Builder builder)632     private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
633         Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification");
634 
635         PendingIntent acceptVideoPendingIntent = createNotificationPendingIntent(
636                 mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
637         builder.addAction(0, mContext.getText(R.string.notification_action_accept),
638                 acceptVideoPendingIntent);
639     }
640 
addDismissUpgradeRequestAction(Notification.Builder builder)641     private void addDismissUpgradeRequestAction(Notification.Builder builder) {
642         Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification");
643 
644         PendingIntent declineVideoPendingIntent = createNotificationPendingIntent(
645                 mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
646         builder.addAction(0, mContext.getText(R.string.notification_action_dismiss),
647                 declineVideoPendingIntent);
648     }
649 
650     /**
651      * Adds fullscreen intent to the builder.
652      */
configureFullScreenIntent(Notification.Builder builder, PendingIntent intent, Call call)653     private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent,
654             Call call) {
655         // Ok, we actually want to launch the incoming call
656         // UI at this point (in addition to simply posting a notification
657         // to the status bar).  Setting fullScreenIntent will cause
658         // the InCallScreen to be launched immediately *unless* the
659         // current foreground activity is marked as "immersive".
660         Log.d(this, "- Setting fullScreenIntent: " + intent);
661         builder.setFullScreenIntent(intent, true);
662 
663         // Ugly hack alert:
664         //
665         // The NotificationManager has the (undocumented) behavior
666         // that it will *ignore* the fullScreenIntent field if you
667         // post a new Notification that matches the ID of one that's
668         // already active.  Unfortunately this is exactly what happens
669         // when you get an incoming call-waiting call:  the
670         // "ongoing call" notification is already visible, so the
671         // InCallScreen won't get launched in this case!
672         // (The result: if you bail out of the in-call UI while on a
673         // call and then get a call-waiting call, the incoming call UI
674         // won't come up automatically.)
675         //
676         // The workaround is to just notice this exact case (this is a
677         // call-waiting call *and* the InCallScreen is not in the
678         // foreground) and manually cancel the in-call notification
679         // before (re)posting it.
680         //
681         // TODO: there should be a cleaner way of avoiding this
682         // problem (see discussion in bug 3184149.)
683 
684         // If a call is onhold during an incoming call, the call actually comes in as
685         // INCOMING.  For that case *and* traditional call-waiting, we want to
686         // cancel the notification.
687         boolean isCallWaiting = (call.getState() == Call.State.CALL_WAITING ||
688                 (call.getState() == Call.State.INCOMING &&
689                         CallList.getInstance().getBackgroundCall() != null));
690 
691         if (isCallWaiting) {
692             Log.i(this, "updateInCallNotification: call-waiting! force relaunch...");
693             // Cancel the IN_CALL_NOTIFICATION immediately before
694             // (re)posting it; this seems to force the
695             // NotificationManager to launch the fullScreenIntent.
696             mNotificationManager.cancel(NOTIFICATION_IN_CALL);
697         }
698     }
699 
getNotificationBuilder()700     private Notification.Builder getNotificationBuilder() {
701         final Notification.Builder builder = new Notification.Builder(mContext);
702         builder.setOngoing(true);
703 
704         // Make the notification prioritized over the other normal notifications.
705         builder.setPriority(Notification.PRIORITY_HIGH);
706 
707         return builder;
708     }
709 
createLaunchPendingIntent()710     private PendingIntent createLaunchPendingIntent() {
711 
712         final Intent intent = InCallPresenter.getInstance().getInCallIntent(
713                 false /* showDialpad */, false /* newOutgoingCall */);
714 
715         // PendingIntent that can be used to launch the InCallActivity.  The
716         // system fires off this intent if the user pulls down the windowshade
717         // and clicks the notification's expanded view.  It's also used to
718         // launch the InCallActivity immediately when when there's an incoming
719         // call (see the "fullScreenIntent" field below).
720         PendingIntent inCallPendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
721 
722         return inCallPendingIntent;
723     }
724 
725     /**
726      * Returns PendingIntent for answering a phone call. This will typically be used from
727      * Notification context.
728      */
createNotificationPendingIntent(Context context, String action)729     private static PendingIntent createNotificationPendingIntent(Context context, String action) {
730         final Intent intent = new Intent(action, null,
731                 context, NotificationBroadcastReceiver.class);
732         return PendingIntent.getBroadcast(context, 0, intent, 0);
733     }
734 
735     @Override
onCallChanged(Call call)736     public void onCallChanged(Call call) {
737         if (CallList.getInstance().getIncomingCall() == null) {
738             mDialerRingtoneManager.stopCallWaitingTone();
739         }
740     }
741 
742     /**
743      * Responds to changes in the session modification state for the call by dismissing the
744      * status bar notification as required.
745      *
746      * @param sessionModificationState The new session modification state.
747      */
748     @Override
onSessionModificationStateChange(int sessionModificationState)749     public void onSessionModificationStateChange(int sessionModificationState) {
750         if (sessionModificationState == Call.SessionModificationState.NO_REQUEST) {
751             if (mCallId != null) {
752                 CallList.getInstance().removeCallUpdateListener(mCallId, this);
753             }
754 
755             updateNotification(mInCallState, CallList.getInstance());
756         }
757     }
758 
759     @Override
onLastForwardedNumberChange()760     public void onLastForwardedNumberChange() {
761         // no-op
762     }
763 
764     @Override
onChildNumberChange()765     public void onChildNumberChange() {
766         // no-op
767     }
768 }
769