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