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 android.telecom.Call.Details.PROPERTY_HIGH_DEF_AUDIO;
20 import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
21 import static com.android.dialer.app.DevicePolicyResources.NOTIFICATION_INCOMING_WORK_CALL_TITLE;
22 import static com.android.dialer.app.DevicePolicyResources.NOTIFICATION_ONGOING_WORK_CALL_TITLE;
23 import static com.android.dialer.app.DevicePolicyResources.NOTIFICATION_WIFI_WORK_CALL_LABEL;
24 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST;
25 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_SPEAKEASY_CALL;
26 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL;
27 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL;
28 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL;
29 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
30 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
31 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_TURN_OFF_SPEAKER;
32 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_TURN_ON_SPEAKER;
33 
34 import android.Manifest;
35 import android.app.Notification;
36 import android.app.PendingIntent;
37 import android.app.admin.DevicePolicyManager;
38 import android.content.Context;
39 import android.content.Intent;
40 import android.content.res.Resources;
41 import android.graphics.Bitmap;
42 import android.graphics.drawable.BitmapDrawable;
43 import android.graphics.drawable.Drawable;
44 import android.graphics.drawable.Icon;
45 import android.media.AudioAttributes;
46 import android.net.Uri;
47 import android.os.Build.VERSION;
48 import android.os.Build.VERSION_CODES;
49 import android.os.Trace;
50 import android.support.annotation.ColorRes;
51 import android.support.annotation.NonNull;
52 import android.support.annotation.Nullable;
53 import android.support.annotation.RequiresPermission;
54 import android.support.annotation.StringRes;
55 import android.support.annotation.VisibleForTesting;
56 import android.support.v4.os.BuildCompat;
57 import android.telecom.Call.Details;
58 import android.telecom.CallAudioState;
59 import android.telecom.PhoneAccount;
60 import android.telecom.TelecomManager;
61 import android.telecom.VideoProfile;
62 import android.text.BidiFormatter;
63 import android.text.Spannable;
64 import android.text.SpannableString;
65 import android.text.Spanned;
66 import android.text.TextDirectionHeuristics;
67 import android.text.TextUtils;
68 import android.text.style.ForegroundColorSpan;
69 import com.android.contacts.common.ContactsUtils;
70 import com.android.contacts.common.ContactsUtils.UserType;
71 import com.android.dialer.common.Assert;
72 import com.android.dialer.common.LogUtil;
73 import com.android.dialer.configprovider.ConfigProviderComponent;
74 import com.android.dialer.contactphoto.BitmapUtil;
75 import com.android.dialer.contacts.ContactsComponent;
76 import com.android.dialer.enrichedcall.EnrichedCallManager;
77 import com.android.dialer.enrichedcall.Session;
78 import com.android.dialer.lettertile.LetterTileDrawable;
79 import com.android.dialer.lettertile.LetterTileDrawable.ContactType;
80 import com.android.dialer.multimedia.MultimediaData;
81 import com.android.dialer.notification.NotificationChannelId;
82 import com.android.dialer.oem.MotorolaUtils;
83 import com.android.dialer.theme.base.ThemeComponent;
84 import com.android.dialer.util.DrawableConverter;
85 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
86 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
87 import com.android.incallui.InCallPresenter.InCallState;
88 import com.android.incallui.async.PausableExecutorImpl;
89 import com.android.incallui.audiomode.AudioModeProvider;
90 import com.android.incallui.call.CallList;
91 import com.android.incallui.call.DialerCall;
92 import com.android.incallui.call.DialerCallListener;
93 import com.android.incallui.call.TelecomAdapter;
94 import com.android.incallui.call.state.DialerCallState;
95 import com.android.incallui.ringtone.DialerRingtoneManager;
96 import com.android.incallui.ringtone.InCallTonePlayer;
97 import com.android.incallui.ringtone.ToneGeneratorFactory;
98 import com.android.incallui.speakeasy.SpeakEasyComponent;
99 import com.android.incallui.videotech.utils.SessionModificationState;
100 import com.google.common.base.Optional;
101 import java.util.Objects;
102 
103 /** This class adds Notifications to the status bar for the in-call experience. */
104 public class StatusBarNotifier
105     implements InCallPresenter.InCallStateListener,
106         EnrichedCallManager.StateChangedListener,
107         ContactInfoCacheCallback {
108 
109   private static final int NOTIFICATION_ID = 1;
110 
111   // Notification types
112   // Indicates that no notification is currently showing.
113   private static final int NOTIFICATION_NONE = 0;
114   // Notification for an active call. This is non-interruptive, but cannot be dismissed.
115   private static final int NOTIFICATION_IN_CALL = 1;
116   // Notification for incoming calls. This is interruptive and will show up as a HUN.
117   private static final int NOTIFICATION_INCOMING_CALL = 2;
118   // Notification for incoming calls in the case where there is already an active call.
119   // This is non-interruptive, but otherwise behaves the same as NOTIFICATION_INCOMING_CALL
120   private static final int NOTIFICATION_INCOMING_CALL_QUIET = 3;
121 
122   private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000};
123 
124   private final Context context;
125   private final ContactInfoCache contactInfoCache;
126   private final DialerRingtoneManager dialerRingtoneManager;
127   private int currentNotification = NOTIFICATION_NONE;
128   private int callState = DialerCallState.INVALID;
129   private int videoState = VideoProfile.STATE_AUDIO_ONLY;
130   private int savedIcon = 0;
131   private String savedContent = null;
132   private Bitmap savedLargeIcon;
133   private String savedContentTitle;
134   private CallAudioState savedCallAudioState;
135   private Uri ringtone;
136   private StatusBarCallListener statusBarCallListener;
137 
StatusBarNotifier(@onNull Context context, @NonNull ContactInfoCache contactInfoCache)138   public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
139     Trace.beginSection("StatusBarNotifier.Constructor");
140     this.context = Assert.isNotNull(context);
141     this.contactInfoCache = contactInfoCache;
142     dialerRingtoneManager =
143         new DialerRingtoneManager(
144             new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()),
145             CallList.getInstance());
146     currentNotification = NOTIFICATION_NONE;
147     Trace.endSection();
148   }
149 
150   /**
151    * Should only be called from a irrecoverable state where it is necessary to dismiss all
152    * notifications.
153    */
clearAllCallNotifications()154   static void clearAllCallNotifications() {
155     LogUtil.e(
156         "StatusBarNotifier.clearAllCallNotifications",
157         "something terrible happened, clear all InCall notifications");
158 
159     TelecomAdapter.getInstance().stopForegroundNotification();
160   }
161 
getWorkStringFromPersonalString(int resId)162   private static int getWorkStringFromPersonalString(int resId) {
163     if (resId == R.string.notification_ongoing_call) {
164       return R.string.notification_ongoing_work_call;
165     } else if (resId == R.string.notification_incoming_call) {
166       return R.string.notification_incoming_work_call;
167     } else {
168       return resId;
169     }
170   }
171 
172   /**
173    * Returns PendingIntent for answering a phone call. This will typically be used from Notification
174    * context.
175    */
createNotificationPendingIntent(Context context, String action)176   private static PendingIntent createNotificationPendingIntent(Context context, String action) {
177     final Intent intent = new Intent(action, null, context, NotificationBroadcastReceiver.class);
178     return PendingIntent.getBroadcast(context, 0, intent, 0);
179   }
180 
181   /** Creates notifications according to the state we receive from {@link InCallPresenter}. */
182   @Override
183   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
onStateChange(InCallState oldState, InCallState newState, CallList callList)184   public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
185     LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState);
186     updateNotification();
187   }
188 
189   @Override
onEnrichedCallStateChanged()190   public void onEnrichedCallStateChanged() {
191     LogUtil.enterBlock("StatusBarNotifier.onEnrichedCallStateChanged");
192     updateNotification();
193   }
194 
195   /**
196    * Updates the phone app's status bar notification *and* launches the incoming call UI in response
197    * to a new incoming call.
198    *
199    * <p>If an incoming call is ringing (or call-waiting), the notification will also include a
200    * "fullScreenIntent" that will cause the InCallScreen to be launched, unless the current
201    * foreground activity is marked as "immersive".
202    *
203    * <p>(This is the mechanism that actually brings up the incoming call UI when we receive a "new
204    * ringing connection" event from the telephony layer.)
205    *
206    * <p>Also note that this method is safe to call even if the phone isn't actually ringing (or,
207    * more likely, if an incoming call *was* ringing briefly but then disconnected). In that case,
208    * we'll simply update or cancel the in-call notification based on the current phone state.
209    *
210    * @see #updateInCallNotification()
211    */
212   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
updateNotification()213   public void updateNotification() {
214     updateInCallNotification();
215   }
216 
217   /**
218    * Take down the in-call notification.
219    *
220    * @see #updateInCallNotification()
221    */
cancelNotification()222   private void cancelNotification() {
223     if (statusBarCallListener != null) {
224       setStatusBarCallListener(null);
225     }
226     if (currentNotification != NOTIFICATION_NONE) {
227       TelecomAdapter.getInstance().stopForegroundNotification();
228       currentNotification = NOTIFICATION_NONE;
229     }
230   }
231 
232   /**
233    * Helper method for updateInCallNotification() and updateNotification(): Update the phone app's
234    * status bar notification based on the current telephony state, or cancels the notification if
235    * the phone is totally idle.
236    */
237   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
updateInCallNotification()238   private void updateInCallNotification() {
239     LogUtil.d("StatusBarNotifier.updateInCallNotification", "");
240 
241     final DialerCall call = getCallToShow(CallList.getInstance());
242 
243     if (call != null) {
244       showNotification(call);
245     } else {
246       cancelNotification();
247     }
248   }
249 
250   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
showNotification(final DialerCall call)251   private void showNotification(final DialerCall call) {
252     Trace.beginSection("StatusBarNotifier.showNotification");
253     final boolean isIncoming =
254         (call.getState() == DialerCallState.INCOMING
255             || call.getState() == DialerCallState.CALL_WAITING);
256     setStatusBarCallListener(new StatusBarCallListener(call));
257 
258     // we make a call to the contact info cache to query for supplemental data to what the
259     // call provides.  This includes the contact name and photo.
260     // This callback will always get called immediately and synchronously with whatever data
261     // it has available, and may make a subsequent call later (same thread) if it had to
262     // call into the contacts provider for more data.
263     contactInfoCache.findInfo(call, isIncoming, this);
264     Trace.endSection();
265   }
266 
267   /** Sets up the main Ui for the notification */
268   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
buildAndSendNotification( CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo)269   private void buildAndSendNotification(
270       CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
271     Trace.beginSection("StatusBarNotifier.buildAndSendNotification");
272     // This can get called to update an existing notification after contact information has come
273     // back. However, it can happen much later. Before we continue, we need to make sure that
274     // the call being passed in is still the one we want to show in the notification.
275     final DialerCall call = getCallToShow(callList);
276     if (call == null || !call.getId().equals(originalCall.getId())) {
277       Trace.endSection();
278       return;
279     }
280 
281     Trace.beginSection("prepare work");
282     final int callState = call.getState();
283     final CallAudioState callAudioState = AudioModeProvider.getInstance().getAudioState();
284 
285     Trace.beginSection("read icon and strings");
286     // Check if data has changed; if nothing is different, don't issue another notification.
287     final int iconResId = getIconToDisplay(call);
288     Bitmap largeIcon = getLargeIconToDisplay(context, contactInfo, call);
289     final CharSequence content = getContentString(call, contactInfo.userType);
290     final String contentTitle = getContentTitle(contactInfo, call);
291     Trace.endSection();
292 
293     final boolean isVideoUpgradeRequest =
294         call.getVideoTech().getSessionModificationState()
295             == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
296     final int notificationType;
297     if (callState == DialerCallState.INCOMING
298         || callState == DialerCallState.CALL_WAITING
299         || isVideoUpgradeRequest) {
300       if (ConfigProviderComponent.get(context)
301           .getConfigProvider()
302           .getBoolean("quiet_incoming_call_if_ui_showing", true)) {
303         notificationType =
304             InCallPresenter.getInstance().isShowingInCallUi()
305                 ? NOTIFICATION_INCOMING_CALL_QUIET
306                 : NOTIFICATION_INCOMING_CALL;
307       } else {
308         boolean alreadyActive =
309             callList.getActiveOrBackgroundCall() != null
310                 && InCallPresenter.getInstance().isShowingInCallUi();
311         notificationType =
312             alreadyActive ? NOTIFICATION_INCOMING_CALL_QUIET : NOTIFICATION_INCOMING_CALL;
313       }
314     } else {
315       notificationType = NOTIFICATION_IN_CALL;
316     }
317     Trace.endSection(); // prepare work
318 
319     if (!checkForChangeAndSaveData(
320         iconResId,
321         content.toString(),
322         largeIcon,
323         contentTitle,
324         callState,
325         call.getVideoState(),
326         notificationType,
327         contactInfo.contactRingtoneUri,
328         callAudioState)) {
329       Trace.endSection();
330       return;
331     }
332 
333     if (largeIcon != null) {
334       largeIcon = getRoundedIcon(largeIcon);
335     }
336 
337     // This builder is used for the notification shown when the device is locked and the user
338     // has set their notification settings to 'hide sensitive content'
339     // {@see Notification.Builder#setPublicVersion}.
340     Notification.Builder publicBuilder = new Notification.Builder(context);
341     publicBuilder
342         .setSmallIcon(iconResId)
343         .setColor(ThemeComponent.get(context).theme().getColorPrimary())
344         // Hide work call state for the lock screen notification
345         .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
346     setNotificationWhen(call, callState, publicBuilder);
347 
348     // Builder for the notification shown when the device is unlocked or the user has set their
349     // notification settings to 'show all notification content'.
350     final Notification.Builder builder = getNotificationBuilder();
351     builder.setPublicVersion(publicBuilder.build());
352 
353     // Set up the main intent to send the user to the in-call screen
354     builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */));
355 
356     LogUtil.i("StatusBarNotifier.buildAndSendNotification", "notificationType=" + notificationType);
357     switch (notificationType) {
358       case NOTIFICATION_INCOMING_CALL:
359         if (BuildCompat.isAtLeastO()) {
360           builder.setChannelId(NotificationChannelId.INCOMING_CALL);
361         }
362         // Set the intent as a full screen intent as well if a call is incoming
363         configureFullScreenIntent(builder, createLaunchPendingIntent(true /* isFullScreen */));
364         // Set the notification category and bump the priority for incoming calls
365         builder.setCategory(Notification.CATEGORY_CALL);
366         // This will be ignored on O+ and handled by the channel
367         builder.setPriority(Notification.PRIORITY_MAX);
368         if (currentNotification != NOTIFICATION_INCOMING_CALL) {
369           LogUtil.i(
370               "StatusBarNotifier.buildAndSendNotification",
371               "Canceling old notification so this one can be noisy");
372           // Moving from a non-interuptive notification (or none) to a noisy one. Cancel the old
373           // notification (if there is one) so the fullScreenIntent or HUN will show
374           TelecomAdapter.getInstance().stopForegroundNotification();
375         }
376         break;
377       case NOTIFICATION_INCOMING_CALL_QUIET:
378         if (BuildCompat.isAtLeastO()) {
379           builder.setChannelId(NotificationChannelId.ONGOING_CALL);
380         }
381         break;
382       case NOTIFICATION_IN_CALL:
383         if (BuildCompat.isAtLeastO()) {
384           publicBuilder.setColorized(true);
385           builder.setColorized(true);
386           builder.setChannelId(NotificationChannelId.ONGOING_CALL);
387         }
388         break;
389       default:
390         break;
391     }
392 
393     // Set the content
394     builder.setContentText(content);
395     builder.setSmallIcon(iconResId);
396     builder.setContentTitle(contentTitle);
397     builder.setLargeIcon(largeIcon);
398     builder.setColor(InCallPresenter.getInstance().getThemeColorManager().getPrimaryColor());
399 
400     if (isVideoUpgradeRequest) {
401       builder.setUsesChronometer(false);
402       addDismissUpgradeRequestAction(builder);
403       addAcceptUpgradeRequestAction(builder);
404     } else {
405       createIncomingCallNotification(call, callState, callAudioState, builder);
406     }
407 
408     addPersonReference(builder, contactInfo, call);
409 
410     Trace.beginSection("fire notification");
411     // Fire off the notification
412     Notification notification = builder.build();
413 
414     if (dialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) {
415       notification.flags |= Notification.FLAG_INSISTENT;
416       notification.sound = contactInfo.contactRingtoneUri;
417       AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder();
418       audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
419       audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
420       notification.audioAttributes = audioAttributes.build();
421       if (dialerRingtoneManager.shouldVibrate(context.getContentResolver())) {
422         notification.vibrate = VIBRATE_PATTERN;
423       }
424     }
425     if (dialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
426       LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone");
427       dialerRingtoneManager.playCallWaitingTone();
428     }
429 
430     LogUtil.i(
431         "StatusBarNotifier.buildAndSendNotification",
432         "displaying notification for " + notificationType);
433 
434     // If a notification exists, this will only update it.
435     TelecomAdapter.getInstance().startForegroundNotification(NOTIFICATION_ID, notification);
436 
437     Trace.endSection();
438     call.getLatencyReport().onNotificationShown();
439     currentNotification = notificationType;
440     Trace.endSection();
441   }
442 
createIncomingCallNotification( DialerCall call, int state, CallAudioState callAudioState, Notification.Builder builder)443   private void createIncomingCallNotification(
444       DialerCall call, int state, CallAudioState callAudioState, Notification.Builder builder) {
445     setNotificationWhen(call, state, builder);
446 
447     // Add hang up option for any active calls (active | onhold), outgoing calls (dialing).
448     if (state == DialerCallState.ACTIVE
449         || state == DialerCallState.ONHOLD
450         || DialerCallState.isDialing(state)) {
451       addHangupAction(builder);
452       addSpeakerAction(builder, callAudioState);
453     } else if (state == DialerCallState.INCOMING || state == DialerCallState.CALL_WAITING) {
454       addDismissAction(builder);
455       if (call.isVideoCall()) {
456         addVideoCallAction(builder);
457       } else {
458         addAnswerAction(builder);
459         addSpeakeasyAnswerAction(builder, call);
460       }
461     }
462   }
463 
464   /**
465    * Sets the notification's when section as needed. For active calls, this is explicitly set as the
466    * duration of the call. For all other states, the notification will automatically show the time
467    * at which the notification was created.
468    */
setNotificationWhen(DialerCall call, int state, Notification.Builder builder)469   private void setNotificationWhen(DialerCall call, int state, Notification.Builder builder) {
470     if (state == DialerCallState.ACTIVE) {
471       builder.setUsesChronometer(true);
472       builder.setWhen(call.getConnectTimeMillis());
473     } else {
474       builder.setUsesChronometer(false);
475     }
476   }
477 
478   /**
479    * Checks the new notification data and compares it against any notification that we are already
480    * displaying. If the data is exactly the same, we return false so that we do not issue a new
481    * notification for the exact same data.
482    */
checkForChangeAndSaveData( int icon, String content, Bitmap largeIcon, String contentTitle, int state, int videoState, int notificationType, Uri ringtone, CallAudioState callAudioState)483   private boolean checkForChangeAndSaveData(
484       int icon,
485       String content,
486       Bitmap largeIcon,
487       String contentTitle,
488       int state,
489       int videoState,
490       int notificationType,
491       Uri ringtone,
492       CallAudioState callAudioState) {
493 
494     // The two are different:
495     // if new title is not null, it should be different from saved version OR
496     // if new title is null, the saved version should not be null
497     final boolean contentTitleChanged =
498         (contentTitle != null && !contentTitle.equals(savedContentTitle))
499             || (contentTitle == null && savedContentTitle != null);
500 
501     boolean largeIconChanged;
502     if (savedLargeIcon == null) {
503       largeIconChanged = largeIcon != null;
504     } else {
505       largeIconChanged = largeIcon == null || !savedLargeIcon.sameAs(largeIcon);
506     }
507 
508     // any change means we are definitely updating
509     boolean retval =
510         (savedIcon != icon)
511             || !Objects.equals(savedContent, content)
512             || (callState != state)
513             || (this.videoState != videoState)
514             || largeIconChanged
515             || contentTitleChanged
516             || !Objects.equals(this.ringtone, ringtone)
517             || !Objects.equals(savedCallAudioState, callAudioState);
518 
519     LogUtil.d(
520         "StatusBarNotifier.checkForChangeAndSaveData",
521         "data changed: icon: %b, content: %b, state: %b, videoState: %b, largeIcon: %b, title: %b,"
522             + "ringtone: %b, audioState: %b, type: %b",
523         (savedIcon != icon),
524         !Objects.equals(savedContent, content),
525         (callState != state),
526         (this.videoState != videoState),
527         largeIconChanged,
528         contentTitleChanged,
529         !Objects.equals(this.ringtone, ringtone),
530         !Objects.equals(savedCallAudioState, callAudioState),
531         currentNotification != notificationType);
532     // If we aren't showing a notification right now or the notification type is changing,
533     // definitely do an update.
534     if (currentNotification != notificationType) {
535       if (currentNotification == NOTIFICATION_NONE) {
536         LogUtil.d(
537             "StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time.");
538       }
539       retval = true;
540     }
541 
542     savedIcon = icon;
543     savedContent = content;
544     callState = state;
545     this.videoState = videoState;
546     savedLargeIcon = largeIcon;
547     savedContentTitle = contentTitle;
548     this.ringtone = ringtone;
549     savedCallAudioState = callAudioState;
550 
551     if (retval) {
552       LogUtil.d(
553           "StatusBarNotifier.checkForChangeAndSaveData", "data changed.  Showing notification");
554     }
555 
556     return retval;
557   }
558 
559   /** Returns the main string to use in the notification. */
560   @VisibleForTesting
561   @Nullable
getContentTitle(ContactCacheEntry contactInfo, DialerCall call)562   String getContentTitle(ContactCacheEntry contactInfo, DialerCall call) {
563     if (call.isConferenceCall()) {
564       return CallerInfoUtils.getConferenceString(
565           context, call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE));
566     }
567 
568     String preferredName =
569         ContactsComponent.get(context)
570             .contactDisplayPreferences()
571             .getDisplayName(contactInfo.namePrimary, contactInfo.nameAlternative);
572     if (TextUtils.isEmpty(preferredName)) {
573       return TextUtils.isEmpty(contactInfo.number)
574           ? null
575           : BidiFormatter.getInstance()
576               .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
577     }
578     return preferredName;
579   }
580 
addPersonReference( Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call)581   private void addPersonReference(
582       Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call) {
583     // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
584     // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
585     // NotificationManager using it.
586     if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
587       builder.addPerson(contactInfo.lookupUri.toString());
588     } else if (!TextUtils.isEmpty(call.getNumber())) {
589       builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null).toString());
590     }
591   }
592 
593   /** Gets a large icon from the contact info object to display in the notification. */
getLargeIconToDisplay( Context context, ContactCacheEntry contactInfo, DialerCall call)594   private static Bitmap getLargeIconToDisplay(
595       Context context, ContactCacheEntry contactInfo, DialerCall call) {
596     Trace.beginSection("StatusBarNotifier.getLargeIconToDisplay");
597     Resources resources = context.getResources();
598     Bitmap largeIcon = null;
599     if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
600       largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
601     }
602     if (contactInfo.photo == null) {
603       int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width);
604       int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height);
605       @ContactType
606       int contactType =
607           LetterTileDrawable.getContactTypeFromPrimitives(
608               call.isVoiceMailNumber(),
609               call.isSpam(),
610               contactInfo.isBusiness,
611               call.getNumberPresentation(),
612               call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE));
613       LetterTileDrawable lettertile = new LetterTileDrawable(resources);
614 
615       lettertile.setCanonicalDialerLetterTileDetails(
616           contactInfo.namePrimary == null ? contactInfo.number : contactInfo.namePrimary,
617           contactInfo.lookupKey,
618           LetterTileDrawable.SHAPE_CIRCLE,
619           contactType);
620       largeIcon = lettertile.getBitmap(width, height);
621     }
622 
623     if (call.isSpam()) {
624       Drawable drawable = resources.getDrawable(R.drawable.blocked_contact, context.getTheme());
625       largeIcon = DrawableConverter.drawableToBitmap(drawable);
626     }
627     Trace.endSection();
628     return largeIcon;
629   }
630 
getRoundedIcon(Bitmap bitmap)631   private Bitmap getRoundedIcon(Bitmap bitmap) {
632     if (bitmap == null) {
633       return null;
634     }
635     final int height =
636         (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_height);
637     final int width =
638         (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_width);
639     return BitmapUtil.getRoundedBitmap(bitmap, width, height);
640   }
641 
642   /**
643    * Returns the appropriate icon res Id to display based on the call for which we want to display
644    * information.
645    */
646   @VisibleForTesting
getIconToDisplay(DialerCall call)647   public int getIconToDisplay(DialerCall call) {
648     // Even if both lines are in use, we only show a single item in
649     // the expanded Notifications UI.  It's labeled "Ongoing call"
650     // (or "On hold" if there's only one call, and it's on hold.)
651     // Also, we don't have room to display caller-id info from two
652     // different calls.  So if both lines are in use, display info
653     // from the foreground call.  And if there's a ringing call,
654     // display that regardless of the state of the other calls.
655     if (call.getState() == DialerCallState.ONHOLD) {
656       return R.drawable.quantum_ic_phone_paused_vd_theme_24;
657     } else if (call.getVideoTech().getSessionModificationState()
658             == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST
659         || call.isVideoCall()) {
660       return R.drawable.quantum_ic_videocam_vd_white_24;
661     } else if (call.hasProperty(PROPERTY_HIGH_DEF_AUDIO)
662         && MotorolaUtils.shouldShowHdIconInNotification(context)) {
663       // Normally when a call is ongoing the status bar displays an icon of a phone. This is a
664       // helpful hint for users so they know how to get back to the call. For Sprint HD calls, we
665       // replace this icon with an icon of a phone with a HD badge. This is a carrier requirement.
666       return R.drawable.ic_hd_call;
667     } else if (call.hasProperty(Details.PROPERTY_HAS_CDMA_VOICE_PRIVACY)) {
668       return R.drawable.quantum_ic_phone_locked_vd_theme_24;
669     }
670     // If ReturnToCall is enabled, use the static icon. The animated one will show in the bubble.
671     if (ReturnToCallController.isEnabled(context)) {
672       return R.drawable.quantum_ic_call_vd_theme_24;
673     } else {
674       return R.drawable.on_going_call;
675     }
676   }
677 
678   /** Returns the message to use with the notification. */
getContentString(DialerCall call, @UserType long userType)679   private CharSequence getContentString(DialerCall call, @UserType long userType) {
680     boolean isIncomingOrWaiting =
681         call.getState() == DialerCallState.INCOMING
682             || call.getState() == DialerCallState.CALL_WAITING;
683 
684     // Is the call placed through work connection service.
685     boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL)
686             || userType == ContactsUtils.USER_TYPE_WORK;
687 
688     if (isIncomingOrWaiting
689         && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) {
690 
691       if (!TextUtils.isEmpty(call.getChildNumber())) {
692         return context.getString(R.string.child_number, call.getChildNumber());
693       } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) {
694         return call.getCallSubject();
695       }
696     }
697 
698 
699     String message = getOngoingCallNotificationMessage(isWorkCall);
700     String wifiBrand = getWifiBrand(isWorkCall);
701 
702     // TODO(a bug): Potentially apply this template logic everywhere.
703     if (call.hasProperty(Details.PROPERTY_WIFI)) {
704       message = context.getString(R.string.notification_ongoing_call_wifi_template, wifiBrand);
705     }
706 
707     if (isIncomingOrWaiting) {
708       if (call.isSpam()) {
709         message = context.getString(R.string.notification_incoming_spam_call);
710       } else if (shouldShowEnrichedCallNotification(call.getEnrichedCallSession())) {
711         message = context.getString(getECIncomingCallText(call.getEnrichedCallSession()));
712       } else if (call.hasProperty(Details.PROPERTY_WIFI)) {
713         message = context.getString(R.string.notification_incoming_call_wifi_template, wifiBrand);
714       } else if (call.getAccountHandle() != null && hasMultiplePhoneAccounts(call)) {
715         return getMultiSimIncomingText(call);
716       } else if (call.isVideoCall()) {
717         message = context.getString(R.string.notification_incoming_video_call);
718       } else {
719         message = getIncomingCallNotificationMessage(isWorkCall);
720       }
721     } else if (call.getState() == DialerCallState.ONHOLD) {
722       message = context.getString(R.string.notification_on_hold);
723     } else if (DialerCallState.isDialing(call.getState())) {
724       message = context.getString(R.string.notification_dialing);
725     } else if (call.isVideoCall()) {
726       message = context.getString(call.getVideoTech().isPaused()
727               ? R.string.notification_ongoing_paused_video_call
728               : R.string.notification_ongoing_video_call);
729     } else if (call.getVideoTech().getSessionModificationState()
730         == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
731       message = context.getString(R.string.notification_requesting_video_call);
732     }
733 
734     return message;
735   }
736 
getOngoingCallNotificationMessage(boolean isWorkCall)737   private String getOngoingCallNotificationMessage(boolean isWorkCall) {
738     if (isWorkCall) {
739       DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
740       return dpm.getResources().getString(NOTIFICATION_ONGOING_WORK_CALL_TITLE, () ->
741               context.getString(R.string.notification_ongoing_work_call));
742     } else {
743       return context.getString(R.string.notification_ongoing_call);
744     }
745   }
746 
getIncomingCallNotificationMessage(boolean isWorkCall)747   private String getIncomingCallNotificationMessage(boolean isWorkCall) {
748     if (isWorkCall) {
749       DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
750       return dpm.getResources().getString(NOTIFICATION_INCOMING_WORK_CALL_TITLE, () ->
751               context.getString(R.string.notification_incoming_work_call));
752     } else {
753       return context.getString(R.string.notification_incoming_call);
754     }
755   }
756 
getWifiBrand(boolean isWorkCall)757   private String getWifiBrand(boolean isWorkCall) {
758     if (isWorkCall) {
759       DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
760       return dpm.getResources().getString(NOTIFICATION_WIFI_WORK_CALL_LABEL, () ->
761               context.getString(R.string.notification_call_wifi_work_brand));
762     } else {
763       return context.getString(R.string.notification_call_wifi_brand);
764     }
765   }
766 
shouldShowEnrichedCallNotification(Session session)767   private boolean shouldShowEnrichedCallNotification(Session session) {
768     if (session == null) {
769       return false;
770     }
771     return session.getMultimediaData().hasData() || session.getMultimediaData().isImportant();
772   }
773 
getECIncomingCallText(Session session)774   private int getECIncomingCallText(Session session) {
775     int resId;
776     MultimediaData data = session.getMultimediaData();
777     boolean hasImage = data.hasImageData();
778     boolean hasSubject = !TextUtils.isEmpty(data.getText());
779     boolean hasMap = data.getLocation() != null;
780     if (data.isImportant()) {
781       if (hasMap) {
782         if (hasImage) {
783           if (hasSubject) {
784             resId = R.string.important_notification_incoming_call_with_photo_message_location;
785           } else {
786             resId = R.string.important_notification_incoming_call_with_photo_location;
787           }
788         } else if (hasSubject) {
789           resId = R.string.important_notification_incoming_call_with_message_location;
790         } else {
791           resId = R.string.important_notification_incoming_call_with_location;
792         }
793       } else if (hasImage) {
794         if (hasSubject) {
795           resId = R.string.important_notification_incoming_call_with_photo_message;
796         } else {
797           resId = R.string.important_notification_incoming_call_with_photo;
798         }
799       } else if (hasSubject) {
800         resId = R.string.important_notification_incoming_call_with_message;
801       } else {
802         resId = R.string.important_notification_incoming_call;
803       }
804       if (context.getString(resId).length() > 50) {
805         resId = R.string.important_notification_incoming_call_attachments;
806       }
807     } else {
808       if (hasMap) {
809         if (hasImage) {
810           if (hasSubject) {
811             resId = R.string.notification_incoming_call_with_photo_message_location;
812           } else {
813             resId = R.string.notification_incoming_call_with_photo_location;
814           }
815         } else if (hasSubject) {
816           resId = R.string.notification_incoming_call_with_message_location;
817         } else {
818           resId = R.string.notification_incoming_call_with_location;
819         }
820       } else if (hasImage) {
821         if (hasSubject) {
822           resId = R.string.notification_incoming_call_with_photo_message;
823         } else {
824           resId = R.string.notification_incoming_call_with_photo;
825         }
826       } else {
827         resId = R.string.notification_incoming_call_with_message;
828       }
829     }
830     if (context.getString(resId).length() > 50) {
831       resId = R.string.notification_incoming_call_attachments;
832     }
833     return resId;
834   }
835 
getMultiSimIncomingText(DialerCall call)836   private CharSequence getMultiSimIncomingText(DialerCall call) {
837     PhoneAccount phoneAccount =
838         context.getSystemService(TelecomManager.class).getPhoneAccount(call.getAccountHandle());
839     if (phoneAccount == null) {
840       return context.getString(R.string.notification_incoming_call);
841     }
842     SpannableString string =
843         new SpannableString(
844             context.getString(
845                 R.string.notification_incoming_call_mutli_sim, phoneAccount.getLabel()));
846     int accountStart = string.toString().lastIndexOf(phoneAccount.getLabel().toString());
847     int accountEnd = accountStart + phoneAccount.getLabel().length();
848 
849     string.setSpan(
850         new ForegroundColorSpan(phoneAccount.getHighlightColor()),
851         accountStart,
852         accountEnd,
853         Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
854     return string;
855   }
856 
857   /** Gets the most relevant call to display in the notification. */
getCallToShow(CallList callList)858   private DialerCall getCallToShow(CallList callList) {
859     if (callList == null) {
860       return null;
861     }
862     DialerCall call = callList.getIncomingCall();
863     if (call == null) {
864       call = callList.getOutgoingCall();
865     }
866     if (call == null) {
867       call = callList.getVideoUpgradeRequestCall();
868     }
869     if (call == null) {
870       call = callList.getActiveOrBackgroundCall();
871     }
872     return call;
873   }
874 
getActionText(@tringRes int stringRes, @ColorRes int colorRes)875   private Spannable getActionText(@StringRes int stringRes, @ColorRes int colorRes) {
876     Spannable spannable = new SpannableString(context.getText(stringRes));
877     if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
878       // This will only work for cases where the Notification.Builder has a fullscreen intent set
879       // Notification.Builder that does not have a full screen intent will take the color of the
880       // app and the following leads to a no-op.
881       spannable.setSpan(
882           new ForegroundColorSpan(context.getColor(colorRes)), 0, spannable.length(), 0);
883     }
884     return spannable;
885   }
886 
addAnswerAction(Notification.Builder builder)887   private void addAnswerAction(Notification.Builder builder) {
888     LogUtil.d(
889         "StatusBarNotifier.addAnswerAction",
890         "will show \"answer\" action in the incoming call Notification");
891     PendingIntent answerVoicePendingIntent =
892         createNotificationPendingIntent(context, ACTION_ANSWER_VOICE_INCOMING_CALL);
893     builder.addAction(
894         new Notification.Action.Builder(
895                 Icon.createWithResource(context, R.drawable.quantum_ic_call_white_24),
896                 getActionText(
897                     R.string.notification_action_answer, R.color.notification_action_accept),
898                 answerVoicePendingIntent)
899             .build());
900   }
901 
addSpeakeasyAnswerAction(Notification.Builder builder, DialerCall call)902   private void addSpeakeasyAnswerAction(Notification.Builder builder, DialerCall call) {
903     if (!call.isSpeakEasyEligible()) {
904       return;
905     }
906 
907     if (!ConfigProviderComponent.get(context)
908         .getConfigProvider()
909         .getBoolean("enable_speakeasy_notification_button", false)) {
910       return;
911     }
912 
913     if (!SpeakEasyComponent.get(context).speakEasyCallManager().isAvailable(context)) {
914       return;
915     }
916 
917     Optional<Integer> buttonText = SpeakEasyComponent.get(context).speakEasyTextResource();
918     if (!buttonText.isPresent()) {
919       return;
920     }
921 
922     LogUtil.d("StatusBarNotifier.addSpeakeasyAnswerAction", "showing button");
923     PendingIntent answerVoicePendingIntent =
924         createNotificationPendingIntent(context, ACTION_ANSWER_SPEAKEASY_CALL);
925 
926     Spannable spannable = new SpannableString(context.getText(buttonText.get()));
927     // TODO(erfanian): Migrate these color values to somewhere more permanent in subsequent
928     // implementation.
929     spannable.setSpan(
930         new ForegroundColorSpan(
931             context.getColor(R.color.DO_NOT_USE_OR_I_WILL_BREAK_YOU_text_span_tertiary_button)),
932         0,
933         spannable.length(),
934         Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
935 
936     builder.addAction(
937         new Notification.Action.Builder(
938                 Icon.createWithResource(context, R.drawable.quantum_ic_call_white_24),
939                 spannable,
940                 answerVoicePendingIntent)
941             .build());
942   }
943 
addDismissAction(Notification.Builder builder)944   private void addDismissAction(Notification.Builder builder) {
945     LogUtil.d(
946         "StatusBarNotifier.addDismissAction",
947         "will show \"decline\" action in the incoming call Notification");
948     PendingIntent declinePendingIntent =
949         createNotificationPendingIntent(context, ACTION_DECLINE_INCOMING_CALL);
950     builder.addAction(
951         new Notification.Action.Builder(
952                 Icon.createWithResource(context, R.drawable.quantum_ic_close_white_24),
953                 getActionText(
954                     R.string.notification_action_dismiss, R.color.notification_action_dismiss),
955                 declinePendingIntent)
956             .build());
957   }
958 
addHangupAction(Notification.Builder builder)959   private void addHangupAction(Notification.Builder builder) {
960     LogUtil.d(
961         "StatusBarNotifier.addHangupAction",
962         "will show \"hang-up\" action in the ongoing active call Notification");
963     PendingIntent hangupPendingIntent =
964         createNotificationPendingIntent(context, ACTION_HANG_UP_ONGOING_CALL);
965     builder.addAction(
966         new Notification.Action.Builder(
967                 Icon.createWithResource(context, R.drawable.quantum_ic_call_end_white_24),
968                 context.getText(R.string.notification_action_end_call),
969                 hangupPendingIntent)
970             .build());
971   }
972 
addSpeakerAction(Notification.Builder builder, CallAudioState callAudioState)973   private void addSpeakerAction(Notification.Builder builder, CallAudioState callAudioState) {
974     if ((callAudioState.getSupportedRouteMask() & CallAudioState.ROUTE_BLUETOOTH)
975         == CallAudioState.ROUTE_BLUETOOTH) {
976       // Don't add speaker button if bluetooth is connected
977       return;
978     }
979     if (callAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
980       addSpeakerOffAction(builder);
981     } else if ((callAudioState.getRoute() & CallAudioState.ROUTE_WIRED_OR_EARPIECE) != 0) {
982       addSpeakerOnAction(builder);
983     }
984   }
985 
addSpeakerOnAction(Notification.Builder builder)986   private void addSpeakerOnAction(Notification.Builder builder) {
987     LogUtil.d(
988         "StatusBarNotifier.addSpeakerOnAction",
989         "will show \"Speaker on\" action in the ongoing active call Notification");
990     PendingIntent speakerOnPendingIntent =
991         createNotificationPendingIntent(context, ACTION_TURN_ON_SPEAKER);
992     builder.addAction(
993         new Notification.Action.Builder(
994                 Icon.createWithResource(context, R.drawable.quantum_ic_volume_up_vd_theme_24),
995                 context.getText(R.string.notification_action_speaker_on),
996                 speakerOnPendingIntent)
997             .build());
998   }
999 
addSpeakerOffAction(Notification.Builder builder)1000   private void addSpeakerOffAction(Notification.Builder builder) {
1001     LogUtil.d(
1002         "StatusBarNotifier.addSpeakerOffAction",
1003         "will show \"Speaker off\" action in the ongoing active call Notification");
1004     PendingIntent speakerOffPendingIntent =
1005         createNotificationPendingIntent(context, ACTION_TURN_OFF_SPEAKER);
1006     builder.addAction(
1007         new Notification.Action.Builder(
1008                 Icon.createWithResource(context, R.drawable.quantum_ic_phone_in_talk_vd_theme_24),
1009                 context.getText(R.string.notification_action_speaker_off),
1010                 speakerOffPendingIntent)
1011             .build());
1012   }
1013 
addVideoCallAction(Notification.Builder builder)1014   private void addVideoCallAction(Notification.Builder builder) {
1015     LogUtil.i(
1016         "StatusBarNotifier.addVideoCallAction",
1017         "will show \"video\" action in the incoming call Notification");
1018     PendingIntent answerVideoPendingIntent =
1019         createNotificationPendingIntent(context, ACTION_ANSWER_VIDEO_INCOMING_CALL);
1020     builder.addAction(
1021         new Notification.Action.Builder(
1022                 Icon.createWithResource(context, R.drawable.quantum_ic_videocam_vd_white_24),
1023                 getActionText(
1024                     R.string.notification_action_answer_video,
1025                     R.color.notification_action_answer_video),
1026                 answerVideoPendingIntent)
1027             .build());
1028   }
1029 
addAcceptUpgradeRequestAction(Notification.Builder builder)1030   private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
1031     LogUtil.i(
1032         "StatusBarNotifier.addAcceptUpgradeRequestAction",
1033         "will show \"accept upgrade\" action in the incoming call Notification");
1034     PendingIntent acceptVideoPendingIntent =
1035         createNotificationPendingIntent(context, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
1036     builder.addAction(
1037         new Notification.Action.Builder(
1038                 Icon.createWithResource(context, R.drawable.quantum_ic_videocam_vd_white_24),
1039                 getActionText(
1040                     R.string.notification_action_accept, R.color.notification_action_accept),
1041                 acceptVideoPendingIntent)
1042             .build());
1043   }
1044 
addDismissUpgradeRequestAction(Notification.Builder builder)1045   private void addDismissUpgradeRequestAction(Notification.Builder builder) {
1046     LogUtil.i(
1047         "StatusBarNotifier.addDismissUpgradeRequestAction",
1048         "will show \"dismiss upgrade\" action in the incoming call Notification");
1049     PendingIntent declineVideoPendingIntent =
1050         createNotificationPendingIntent(context, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
1051     builder.addAction(
1052         new Notification.Action.Builder(
1053                 Icon.createWithResource(context, R.drawable.quantum_ic_videocam_vd_white_24),
1054                 getActionText(
1055                     R.string.notification_action_dismiss, R.color.notification_action_dismiss),
1056                 declineVideoPendingIntent)
1057             .build());
1058   }
1059 
1060   /** Adds fullscreen intent to the builder. */
configureFullScreenIntent(Notification.Builder builder, PendingIntent intent)1061   private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent) {
1062     // Ok, we actually want to launch the incoming call
1063     // UI at this point (in addition to simply posting a notification
1064     // to the status bar).  Setting fullScreenIntent will cause
1065     // the InCallScreen to be launched immediately *unless* the
1066     // current foreground activity is marked as "immersive".
1067     LogUtil.d("StatusBarNotifier.configureFullScreenIntent", "setting fullScreenIntent: " + intent);
1068     builder.setFullScreenIntent(intent, true);
1069   }
1070 
getNotificationBuilder()1071   private Notification.Builder getNotificationBuilder() {
1072     final Notification.Builder builder = new Notification.Builder(context);
1073     builder.setOngoing(true);
1074     builder.setOnlyAlertOnce(true);
1075     // This will be ignored on O+ and handled by the channel
1076     // noinspection deprecation
1077     builder.setPriority(Notification.PRIORITY_HIGH);
1078 
1079     return builder;
1080   }
1081 
createLaunchPendingIntent(boolean isFullScreen)1082   private PendingIntent createLaunchPendingIntent(boolean isFullScreen) {
1083     Intent intent =
1084         InCallActivity.getIntent(
1085             context, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen);
1086 
1087     int requestCode = InCallActivity.PendingIntentRequestCodes.NON_FULL_SCREEN;
1088     if (isFullScreen) {
1089       // Use a unique request code so that the pending intent isn't clobbered by the
1090       // non-full screen pending intent.
1091       requestCode = InCallActivity.PendingIntentRequestCodes.FULL_SCREEN;
1092     }
1093 
1094     // PendingIntent that can be used to launch the InCallActivity.  The
1095     // system fires off this intent if the user pulls down the windowshade
1096     // and clicks the notification's expanded view.  It's also used to
1097     // launch the InCallActivity immediately when when there's an incoming
1098     // call (see the "fullScreenIntent" field below).
1099     return PendingIntent.getActivity(context, requestCode, intent, 0);
1100   }
1101 
setStatusBarCallListener(StatusBarCallListener listener)1102   private void setStatusBarCallListener(StatusBarCallListener listener) {
1103     if (statusBarCallListener != null) {
1104       statusBarCallListener.cleanup();
1105     }
1106     statusBarCallListener = listener;
1107   }
1108 
hasMultiplePhoneAccounts(DialerCall call)1109   private boolean hasMultiplePhoneAccounts(DialerCall call) {
1110     if (call.getCallCapableAccounts() == null) {
1111       return false;
1112     }
1113     return call.getCallCapableAccounts().size() > 1;
1114   }
1115 
1116   @Override
1117   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
onContactInfoComplete(String callId, ContactCacheEntry entry)1118   public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
1119     DialerCall call = CallList.getInstance().getCallById(callId);
1120     if (call != null) {
1121       call.getLogState().contactLookupResult = entry.contactLookupResult;
1122       buildAndSendNotification(CallList.getInstance(), call, entry);
1123     }
1124   }
1125 
1126   @Override
1127   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
onImageLoadComplete(String callId, ContactCacheEntry entry)1128   public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
1129     DialerCall call = CallList.getInstance().getCallById(callId);
1130     if (call != null) {
1131       buildAndSendNotification(CallList.getInstance(), call, entry);
1132     }
1133   }
1134 
1135   private class StatusBarCallListener implements DialerCallListener {
1136 
1137     private DialerCall dialerCall;
1138 
StatusBarCallListener(DialerCall dialerCall)1139     StatusBarCallListener(DialerCall dialerCall) {
1140       this.dialerCall = dialerCall;
1141       this.dialerCall.addListener(this);
1142     }
1143 
cleanup()1144     void cleanup() {
1145       dialerCall.removeListener(this);
1146     }
1147 
1148     @Override
onDialerCallDisconnect()1149     public void onDialerCallDisconnect() {}
1150 
1151     @Override
onDialerCallUpdate()1152     public void onDialerCallUpdate() {
1153       if (CallList.getInstance().getIncomingCall() == null) {
1154         dialerRingtoneManager.stopCallWaitingTone();
1155       }
1156     }
1157 
1158     @Override
onDialerCallChildNumberChange()1159     public void onDialerCallChildNumberChange() {}
1160 
1161     @Override
onDialerCallLastForwardedNumberChange()1162     public void onDialerCallLastForwardedNumberChange() {}
1163 
1164     @Override
onDialerCallUpgradeToVideo()1165     public void onDialerCallUpgradeToVideo() {}
1166 
1167     @Override
onWiFiToLteHandover()1168     public void onWiFiToLteHandover() {}
1169 
1170     @Override
onHandoverToWifiFailure()1171     public void onHandoverToWifiFailure() {}
1172 
1173     @Override
onInternationalCallOnWifi()1174     public void onInternationalCallOnWifi() {}
1175 
1176     @Override
onEnrichedCallSessionUpdate()1177     public void onEnrichedCallSessionUpdate() {}
1178 
1179     /**
1180      * Responds to changes in the session modification state for the call by dismissing the status
1181      * bar notification as required.
1182      */
1183     @Override
onDialerCallSessionModificationStateChange()1184     public void onDialerCallSessionModificationStateChange() {
1185       if (dialerCall.getVideoTech().getSessionModificationState()
1186           == SessionModificationState.NO_REQUEST) {
1187         cleanup();
1188         updateNotification();
1189       }
1190     }
1191   }
1192 }
1193