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