CallCardPresenter.java revision 9e335e2d4fb43b22c7f95b2e9d4e048798e8e239
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
17package com.android.incallui;
18
19import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
20
21import android.Manifest;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.pm.ApplicationInfo;
26import android.content.pm.PackageManager;
27import android.graphics.drawable.Drawable;
28import android.hardware.display.DisplayManager;
29import android.os.BatteryManager;
30import android.os.Handler;
31import android.os.Trace;
32import android.support.annotation.NonNull;
33import android.support.annotation.Nullable;
34import android.support.v4.app.Fragment;
35import android.support.v4.content.ContextCompat;
36import android.telecom.Call.Details;
37import android.telecom.StatusHints;
38import android.telecom.TelecomManager;
39import android.text.TextUtils;
40import android.view.Display;
41import android.view.View;
42import android.view.accessibility.AccessibilityEvent;
43import android.view.accessibility.AccessibilityManager;
44import com.android.contacts.common.ContactsUtils;
45import com.android.contacts.common.preference.ContactsPreferences;
46import com.android.contacts.common.util.ContactDisplayUtils;
47import com.android.dialer.common.Assert;
48import com.android.dialer.common.LogUtil;
49import com.android.dialer.compat.ActivityCompat;
50import com.android.dialer.configprovider.ConfigProviderBindings;
51import com.android.dialer.logging.DialerImpression;
52import com.android.dialer.logging.Logger;
53import com.android.dialer.multimedia.MultimediaData;
54import com.android.dialer.oem.MotorolaUtils;
55import com.android.dialer.phonenumberutil.PhoneNumberHelper;
56import com.android.dialer.postcall.PostCall;
57import com.android.incallui.ContactInfoCache.ContactCacheEntry;
58import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
59import com.android.incallui.InCallPresenter.InCallDetailsListener;
60import com.android.incallui.InCallPresenter.InCallEventListener;
61import com.android.incallui.InCallPresenter.InCallState;
62import com.android.incallui.InCallPresenter.InCallStateListener;
63import com.android.incallui.InCallPresenter.IncomingCallListener;
64import com.android.incallui.call.CallList;
65import com.android.incallui.call.DialerCall;
66import com.android.incallui.call.DialerCall.State;
67import com.android.incallui.call.DialerCallListener;
68import com.android.incallui.calllocation.CallLocation;
69import com.android.incallui.calllocation.CallLocationComponent;
70import com.android.incallui.incall.protocol.ContactPhotoType;
71import com.android.incallui.incall.protocol.InCallScreen;
72import com.android.incallui.incall.protocol.InCallScreenDelegate;
73import com.android.incallui.incall.protocol.PrimaryCallState;
74import com.android.incallui.incall.protocol.PrimaryCallState.ButtonState;
75import com.android.incallui.incall.protocol.PrimaryInfo;
76import com.android.incallui.incall.protocol.SecondaryInfo;
77import com.android.incallui.videotech.utils.SessionModificationState;
78import java.lang.ref.WeakReference;
79
80/**
81 * Controller for the Call Card Fragment. This class listens for changes to InCallState and passes
82 * it along to the fragment.
83 */
84public class CallCardPresenter
85    implements InCallStateListener,
86        IncomingCallListener,
87        InCallDetailsListener,
88        InCallEventListener,
89        InCallScreenDelegate,
90        DialerCallListener {
91
92  /**
93   * Amount of time to wait before sending an announcement via the accessibility manager. When the
94   * call state changes to an outgoing or incoming state for the first time, the UI can often be
95   * changing due to call updates or contact lookup. This allows the UI to settle to a stable state
96   * to ensure that the correct information is announced.
97   */
98  private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS = 500;
99
100  /** Flag to allow the user's current location to be shown during emergency calls. */
101  private static final String CONFIG_ENABLE_EMERGENCY_LOCATION = "config_enable_emergency_location";
102
103  private static final boolean CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT = true;
104
105  /**
106   * Make it possible to not get location during an emergency call if the battery is too low, since
107   * doing so could trigger gps and thus potentially cause the phone to die in the middle of the
108   * call.
109   */
110  private static final String CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION =
111      "min_battery_percent_for_emergency_location";
112
113  private static final long CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT = 10;
114
115  private final Context context;
116  private final Handler handler = new Handler();
117
118  private DialerCall primary;
119  private String primaryNumber;
120  private DialerCall secondary;
121  private String secondaryNumber;
122  private ContactCacheEntry primaryContactInfo;
123  private ContactCacheEntry secondaryContactInfo;
124  @Nullable private ContactsPreferences contactsPreferences;
125  private boolean isFullscreen = false;
126  private InCallScreen inCallScreen;
127  private boolean isInCallScreenReady;
128  private boolean shouldSendAccessibilityEvent;
129
130  @NonNull private final CallLocation callLocation;
131  private final Runnable sendAccessibilityEventRunnable =
132      new Runnable() {
133        @Override
134        public void run() {
135          shouldSendAccessibilityEvent = !sendAccessibilityEvent(context, getUi());
136          LogUtil.i(
137              "CallCardPresenter.sendAccessibilityEventRunnable",
138              "still should send: %b",
139              shouldSendAccessibilityEvent);
140          if (!shouldSendAccessibilityEvent) {
141            handler.removeCallbacks(this);
142          }
143        }
144      };
145
146  public CallCardPresenter(Context context) {
147    LogUtil.i("CallCardPresenter.constructor", null);
148    this.context = Assert.isNotNull(context).getApplicationContext();
149    callLocation = CallLocationComponent.get(this.context).getCallLocation();
150  }
151
152  private static boolean hasCallSubject(DialerCall call) {
153    return !TextUtils.isEmpty(call.getCallSubject());
154  }
155
156  @Override
157  public void onInCallScreenDelegateInit(InCallScreen inCallScreen) {
158    Assert.isNotNull(inCallScreen);
159    this.inCallScreen = inCallScreen;
160    contactsPreferences = ContactsPreferencesFactory.newContactsPreferences(context);
161
162    // Call may be null if disconnect happened already.
163    DialerCall call = CallList.getInstance().getFirstCall();
164    if (call != null) {
165      primary = call;
166      if (shouldShowNoteSentToast(primary)) {
167        this.inCallScreen.showNoteSentToast();
168      }
169      call.addListener(this);
170      // start processing lookups right away.
171      if (!call.isConferenceCall()) {
172        startContactInfoSearch(call, true, call.getState() == DialerCall.State.INCOMING);
173      } else {
174        updateContactEntry(null, true);
175      }
176    }
177
178    onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance());
179  }
180
181  @Override
182  public void onInCallScreenReady() {
183    LogUtil.i("CallCardPresenter.onInCallScreenReady", null);
184    Assert.checkState(!isInCallScreenReady);
185    if (contactsPreferences != null) {
186      contactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
187    }
188
189    // Contact search may have completed before ui is ready.
190    if (primaryContactInfo != null) {
191      updatePrimaryDisplayInfo();
192    }
193
194    // Register for call state changes last
195    InCallPresenter.getInstance().addListener(this);
196    InCallPresenter.getInstance().addIncomingCallListener(this);
197    InCallPresenter.getInstance().addDetailsListener(this);
198    InCallPresenter.getInstance().addInCallEventListener(this);
199    isInCallScreenReady = true;
200
201    // Log location impressions
202    if (isOutgoingEmergencyCall(primary)) {
203      Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_NEW_EMERGENCY_CALL);
204    } else if (isIncomingEmergencyCall(primary) || isIncomingEmergencyCall(secondary)) {
205      Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_CALLBACK);
206    }
207
208    // Showing the location may have been skipped if the UI wasn't ready during previous layout.
209    if (shouldShowLocation()) {
210      inCallScreen.showLocationUi(getLocationFragment());
211
212      // Log location impressions
213      if (!hasLocationPermission()) {
214        Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_NO_LOCATION_PERMISSION);
215      } else if (isBatteryTooLowForEmergencyLocation()) {
216        Logger.get(context)
217            .logImpression(DialerImpression.Type.EMERGENCY_BATTERY_TOO_LOW_TO_GET_LOCATION);
218      } else if (!callLocation.canGetLocation(context)) {
219        Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_CANT_GET_LOCATION);
220      }
221    }
222  }
223
224  @Override
225  public void onInCallScreenUnready() {
226    LogUtil.i("CallCardPresenter.onInCallScreenUnready", null);
227    Assert.checkState(isInCallScreenReady);
228
229    // stop getting call state changes
230    InCallPresenter.getInstance().removeListener(this);
231    InCallPresenter.getInstance().removeIncomingCallListener(this);
232    InCallPresenter.getInstance().removeDetailsListener(this);
233    InCallPresenter.getInstance().removeInCallEventListener(this);
234    if (primary != null) {
235      primary.removeListener(this);
236    }
237
238    callLocation.close();
239
240    primary = null;
241    primaryContactInfo = null;
242    secondaryContactInfo = null;
243    isInCallScreenReady = false;
244  }
245
246  @Override
247  public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
248    // same logic should happen as with onStateChange()
249    onStateChange(oldState, newState, CallList.getInstance());
250  }
251
252  @Override
253  public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
254    Trace.beginSection("CallCardPresenter.onStateChange");
255    LogUtil.v("CallCardPresenter.onStateChange", "oldState: %s, newState: %s", oldState, newState);
256    if (inCallScreen == null) {
257      Trace.endSection();
258      return;
259    }
260
261    DialerCall primary = null;
262    DialerCall secondary = null;
263
264    if (newState == InCallState.INCOMING) {
265      primary = callList.getIncomingCall();
266    } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) {
267      primary = callList.getOutgoingCall();
268      if (primary == null) {
269        primary = callList.getPendingOutgoingCall();
270      }
271
272      // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
273      // highest priority call to display as the secondary call.
274      secondary = getCallToDisplay(callList, null, true);
275    } else if (newState == InCallState.INCALL) {
276      primary = getCallToDisplay(callList, null, false);
277      secondary = getCallToDisplay(callList, primary, true);
278    }
279
280    LogUtil.v("CallCardPresenter.onStateChange", "primary call: " + primary);
281    LogUtil.v("CallCardPresenter.onStateChange", "secondary call: " + secondary);
282    String primaryNumber = null;
283    String secondaryNumber = null;
284    if (primary != null) {
285      primaryNumber = primary.getNumber();
286    }
287    if (secondary != null) {
288      secondaryNumber = secondary.getNumber();
289    }
290
291    final boolean primaryChanged =
292        !(DialerCall.areSame(this.primary, primary)
293            && TextUtils.equals(this.primaryNumber, primaryNumber));
294    final boolean secondaryChanged =
295        !(DialerCall.areSame(this.secondary, secondary)
296            && TextUtils.equals(this.secondaryNumber, secondaryNumber));
297
298    this.secondary = secondary;
299    this.secondaryNumber = secondaryNumber;
300    DialerCall previousPrimary = this.primary;
301    this.primary = primary;
302    this.primaryNumber = primaryNumber;
303
304    if (this.primary != null) {
305      InCallPresenter.getInstance().onForegroundCallChanged(this.primary);
306      inCallScreen.updateInCallScreenColors();
307    }
308
309    if (primaryChanged && shouldShowNoteSentToast(primary)) {
310      inCallScreen.showNoteSentToast();
311    }
312
313    // Refresh primary call information if either:
314    // 1. Primary call changed.
315    // 2. The call's ability to manage conference has changed.
316    if (shouldRefreshPrimaryInfo(primaryChanged)) {
317      // primary call has changed
318      if (previousPrimary != null) {
319        previousPrimary.removeListener(this);
320      }
321      this.primary.addListener(this);
322
323      primaryContactInfo =
324          ContactInfoCache.buildCacheEntryFromCall(
325              context, this.primary, this.primary.getState() == DialerCall.State.INCOMING);
326      updatePrimaryDisplayInfo();
327      maybeStartSearch(this.primary, true);
328    }
329
330    if (previousPrimary != null && this.primary == null) {
331      previousPrimary.removeListener(this);
332    }
333
334    if (secondaryChanged) {
335      if (this.secondary == null) {
336        // Secondary call may have ended.  Update the ui.
337        secondaryContactInfo = null;
338        updateSecondaryDisplayInfo();
339      } else {
340        // secondary call has changed
341        secondaryContactInfo =
342            ContactInfoCache.buildCacheEntryFromCall(
343                context, this.secondary, this.secondary.getState() == DialerCall.State.INCOMING);
344        updateSecondaryDisplayInfo();
345        maybeStartSearch(this.secondary, false);
346      }
347    }
348
349    // Set the call state
350    int callState = DialerCall.State.IDLE;
351    if (this.primary != null) {
352      callState = this.primary.getState();
353      updatePrimaryCallState();
354    } else {
355      getUi().setCallState(PrimaryCallState.empty());
356    }
357
358    maybeShowManageConferenceCallButton();
359
360    // Hide the end call button instantly if we're receiving an incoming call.
361    getUi()
362        .setEndCallButtonEnabled(
363            shouldShowEndCallButton(this.primary, callState),
364            callState != DialerCall.State.INCOMING /* animate */);
365
366    maybeSendAccessibilityEvent(oldState, newState, primaryChanged);
367    Trace.endSection();
368  }
369
370  @Override
371  public void onDetailsChanged(DialerCall call, Details details) {
372    updatePrimaryCallState();
373
374    if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE)
375        != details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) {
376      maybeShowManageConferenceCallButton();
377    }
378  }
379
380  @Override
381  public void onDialerCallDisconnect() {}
382
383  @Override
384  public void onDialerCallUpdate() {
385    // No-op; specific call updates handled elsewhere.
386  }
387
388  @Override
389  public void onWiFiToLteHandover() {}
390
391  @Override
392  public void onHandoverToWifiFailure() {}
393
394  @Override
395  public void onInternationalCallOnWifi() {}
396
397  @Override
398  public void onEnrichedCallSessionUpdate() {
399    LogUtil.enterBlock("CallCardPresenter.onEnrichedCallSessionUpdate");
400    updatePrimaryDisplayInfo();
401  }
402
403  /** Handles a change to the child number by refreshing the primary call info. */
404  @Override
405  public void onDialerCallChildNumberChange() {
406    LogUtil.v("CallCardPresenter.onDialerCallChildNumberChange", "");
407
408    if (primary == null) {
409      return;
410    }
411    updatePrimaryDisplayInfo();
412  }
413
414  /** Handles a change to the last forwarding number by refreshing the primary call info. */
415  @Override
416  public void onDialerCallLastForwardedNumberChange() {
417    LogUtil.v("CallCardPresenter.onDialerCallLastForwardedNumberChange", "");
418
419    if (primary == null) {
420      return;
421    }
422    updatePrimaryDisplayInfo();
423    updatePrimaryCallState();
424  }
425
426  @Override
427  public void onDialerCallUpgradeToVideo() {}
428
429  /** Handles a change to the session modification state for a call. */
430  @Override
431  public void onDialerCallSessionModificationStateChange() {
432    LogUtil.enterBlock("CallCardPresenter.onDialerCallSessionModificationStateChange");
433
434    if (primary == null) {
435      return;
436    }
437    getUi()
438        .setEndCallButtonEnabled(
439            primary.getVideoTech().getSessionModificationState()
440                != SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
441            true /* shouldAnimate */);
442    updatePrimaryCallState();
443  }
444
445  private boolean shouldRefreshPrimaryInfo(boolean primaryChanged) {
446    if (primary == null) {
447      return false;
448    }
449    return primaryChanged
450        || inCallScreen.isManageConferenceVisible() != shouldShowManageConference();
451  }
452
453  private void updatePrimaryCallState() {
454    if (getUi() != null && primary != null) {
455      boolean isWorkCall =
456          primary.hasProperty(PROPERTY_ENTERPRISE_CALL)
457              || (primaryContactInfo != null
458                  && primaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
459      boolean isHdAudioCall =
460          isPrimaryCallActive() && primary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO);
461      boolean isAttemptingHdAudioCall =
462          !isHdAudioCall
463              && !primary.hasProperty(DialerCall.PROPERTY_CODEC_KNOWN)
464              && MotorolaUtils.shouldBlinkHdIconWhenConnectingCall(context);
465
466      boolean isBusiness = primaryContactInfo != null && primaryContactInfo.isBusiness;
467
468      // Check for video state change and update the visibility of the contact photo.  The contact
469      // photo is hidden when the incoming video surface is shown.
470      // The contact photo visibility can also change in setPrimary().
471      boolean shouldShowContactPhoto =
472          !VideoCallPresenter.showIncomingVideo(primary.getVideoState(), primary.getState());
473      getUi()
474          .setCallState(
475              PrimaryCallState.builder()
476                  .setState(primary.getState())
477                  .setIsVideoCall(primary.isVideoCall())
478                  .setSessionModificationState(primary.getVideoTech().getSessionModificationState())
479                  .setDisconnectCause(primary.getDisconnectCause())
480                  .setConnectionLabel(getConnectionLabel())
481                  .setConnectionIcon(getCallStateIcon())
482                  .setGatewayNumber(getGatewayNumber())
483                  .setCallSubject(shouldShowCallSubject(primary) ? primary.getCallSubject() : null)
484                  .setCallbackNumber(
485                      PhoneNumberHelper.formatNumber(
486                          primary.getCallbackNumber(), primary.getSimCountryIso()))
487                  .setIsWifi(primary.hasProperty(Details.PROPERTY_WIFI))
488                  .setIsConference(
489                      primary.isConferenceCall()
490                          && !primary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE))
491                  .setIsWorkCall(isWorkCall)
492                  .setIsHdAttempting(isAttemptingHdAudioCall)
493                  .setIsHdAudioCall(isHdAudioCall)
494                  .setIsForwardedNumber(
495                      !TextUtils.isEmpty(primary.getLastForwardedNumber())
496                          || primary.isCallForwarded())
497                  .setShouldShowContactPhoto(shouldShowContactPhoto)
498                  .setConnectTimeMillis(primary.getConnectTimeMillis())
499                  .setIsVoiceMailNumber(primary.isVoiceMailNumber())
500                  .setIsRemotelyHeld(primary.isRemotelyHeld())
501                  .setIsBusinessNumber(isBusiness)
502                  .setSupportsCallOnHold(supports2ndCallOnHold())
503                  .setSwapToSecondaryButtonState(getSwapToSecondaryButtonState())
504                  .setIsAssistedDialed(primary.isAssistedDialed())
505                  .setCustomLabel(null)
506                  .setAssistedDialingExtras(primary.getAssistedDialingExtras())
507                  .build());
508
509      InCallActivity activity =
510          (InCallActivity) (inCallScreen.getInCallScreenFragment().getActivity());
511      if (activity != null) {
512        activity.onPrimaryCallStateChanged();
513      }
514    }
515  }
516
517  private @ButtonState int getSwapToSecondaryButtonState() {
518    if (secondary == null) {
519      return ButtonState.NOT_SUPPORT;
520    }
521    if (primary.getState() == State.ACTIVE) {
522      return ButtonState.ENABLED;
523    }
524    return ButtonState.DISABLED;
525  }
526
527  /** Only show the conference call button if we can manage the conference. */
528  private void maybeShowManageConferenceCallButton() {
529    getUi().showManageConferenceCallButton(shouldShowManageConference());
530  }
531
532  /**
533   * Determines if the manage conference button should be visible, based on the current primary
534   * call.
535   *
536   * @return {@code True} if the manage conference button should be visible.
537   */
538  private boolean shouldShowManageConference() {
539    if (primary == null) {
540      return false;
541    }
542
543    return primary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE) && !isFullscreen;
544  }
545
546  private boolean supports2ndCallOnHold() {
547    DialerCall firstCall = CallList.getInstance().getActiveOrBackgroundCall();
548    DialerCall incomingCall = CallList.getInstance().getIncomingCall();
549    if (firstCall != null && incomingCall != null && firstCall != incomingCall) {
550      return incomingCall.can(Details.CAPABILITY_HOLD);
551    }
552    return true;
553  }
554
555  @Override
556  public void onCallStateButtonClicked() {
557    Intent broadcastIntent = Bindings.get(context).getCallStateButtonBroadcastIntent(context);
558    if (broadcastIntent != null) {
559      LogUtil.v(
560          "CallCardPresenter.onCallStateButtonClicked",
561          "sending call state button broadcast: " + broadcastIntent);
562      context.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE);
563    }
564  }
565
566  @Override
567  public void onManageConferenceClicked() {
568    InCallActivity activity =
569        (InCallActivity) (inCallScreen.getInCallScreenFragment().getActivity());
570    activity.showConferenceFragment(true);
571  }
572
573  @Override
574  public void onShrinkAnimationComplete() {
575    InCallPresenter.getInstance().onShrinkAnimationComplete();
576  }
577
578  private void maybeStartSearch(DialerCall call, boolean isPrimary) {
579    // no need to start search for conference calls which show generic info.
580    if (call != null && !call.isConferenceCall()) {
581      startContactInfoSearch(call, isPrimary, call.getState() == DialerCall.State.INCOMING);
582    }
583  }
584
585  /** Starts a query for more contact data for the save primary and secondary calls. */
586  private void startContactInfoSearch(
587      final DialerCall call, final boolean isPrimary, boolean isIncoming) {
588    final ContactInfoCache cache = ContactInfoCache.getInstance(context);
589
590    cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary));
591  }
592
593  private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) {
594    final boolean entryMatchesExistingCall =
595        (isPrimary && primary != null && TextUtils.equals(callId, primary.getId()))
596            || (!isPrimary && secondary != null && TextUtils.equals(callId, secondary.getId()));
597    if (entryMatchesExistingCall) {
598      updateContactEntry(entry, isPrimary);
599    } else {
600      LogUtil.e(
601          "CallCardPresenter.onContactInfoComplete",
602          "dropping stale contact lookup info for " + callId);
603    }
604
605    final DialerCall call = CallList.getInstance().getCallById(callId);
606    if (call != null) {
607      call.getLogState().contactLookupResult = entry.contactLookupResult;
608    }
609    if (entry.lookupUri != null) {
610      CallerInfoUtils.sendViewNotification(context, entry.lookupUri);
611    }
612  }
613
614  private void onImageLoadComplete(String callId, ContactCacheEntry entry) {
615    if (getUi() == null) {
616      return;
617    }
618
619    if (entry.photo != null) {
620      if (primary != null && callId.equals(primary.getId())) {
621        updateContactEntry(entry, true /* isPrimary */);
622      } else if (secondary != null && callId.equals(secondary.getId())) {
623        updateContactEntry(entry, false /* isPrimary */);
624      }
625    }
626  }
627
628  private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) {
629    if (isPrimary) {
630      primaryContactInfo = entry;
631      updatePrimaryDisplayInfo();
632    } else {
633      secondaryContactInfo = entry;
634      updateSecondaryDisplayInfo();
635    }
636  }
637
638  /**
639   * Get the highest priority call to display. Goes through the calls and chooses which to return
640   * based on priority of which type of call to display to the user. Callers can use the "ignore"
641   * feature to get the second best call by passing a previously found primary call as ignore.
642   *
643   * @param ignore A call to ignore if found.
644   */
645  private DialerCall getCallToDisplay(
646      CallList callList, DialerCall ignore, boolean skipDisconnected) {
647    // Active calls come second.  An active call always gets precedent.
648    DialerCall retval = callList.getActiveCall();
649    if (retval != null && retval != ignore) {
650      return retval;
651    }
652
653    // Sometimes there is intemediate state that two calls are in active even one is about
654    // to be on hold.
655    retval = callList.getSecondActiveCall();
656    if (retval != null && retval != ignore) {
657      return retval;
658    }
659
660    // Disconnected calls get primary position if there are no active calls
661    // to let user know quickly what call has disconnected. Disconnected
662    // calls are very short lived.
663    if (!skipDisconnected) {
664      retval = callList.getDisconnectingCall();
665      if (retval != null && retval != ignore) {
666        return retval;
667      }
668      retval = callList.getDisconnectedCall();
669      if (retval != null && retval != ignore) {
670        return retval;
671      }
672    }
673
674    // Then we go to background call (calls on hold)
675    retval = callList.getBackgroundCall();
676    if (retval != null && retval != ignore) {
677      return retval;
678    }
679
680    // Lastly, we go to a second background call.
681    retval = callList.getSecondBackgroundCall();
682
683    return retval;
684  }
685
686  private void updatePrimaryDisplayInfo() {
687    if (inCallScreen == null) {
688      // TODO: May also occur if search result comes back after ui is destroyed. Look into
689      // removing that case completely.
690      LogUtil.v(
691          "CallCardPresenter.updatePrimaryDisplayInfo",
692          "updatePrimaryDisplayInfo called but ui is null!");
693      return;
694    }
695
696    if (primary == null) {
697      // Clear the primary display info.
698      inCallScreen.setPrimary(PrimaryInfo.empty());
699      return;
700    }
701
702    // Hide the contact photo if we are in a video call and the incoming video surface is
703    // showing.
704    boolean showContactPhoto =
705        !VideoCallPresenter.showIncomingVideo(primary.getVideoState(), primary.getState());
706
707    // DialerCall placed through a work phone account.
708    boolean hasWorkCallProperty = primary.hasProperty(PROPERTY_ENTERPRISE_CALL);
709
710    MultimediaData multimediaData = null;
711    if (primary.getEnrichedCallSession() != null) {
712      multimediaData = primary.getEnrichedCallSession().getMultimediaData();
713    }
714
715    if (primary.isConferenceCall()) {
716      LogUtil.v(
717          "CallCardPresenter.updatePrimaryDisplayInfo",
718          "update primary display info for conference call.");
719
720      inCallScreen.setPrimary(
721          PrimaryInfo.builder()
722              .setName(
723                  CallerInfoUtils.getConferenceString(
724                      context, primary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)))
725              .setNameIsNumber(false)
726              .setPhotoType(ContactPhotoType.DEFAULT_PLACEHOLDER)
727              .setIsSipCall(false)
728              .setIsContactPhotoShown(showContactPhoto)
729              .setIsWorkCall(hasWorkCallProperty)
730              .setIsSpam(false)
731              .setIsLocalContact(false)
732              .setAnsweringDisconnectsOngoingCall(false)
733              .setShouldShowLocation(shouldShowLocation())
734              .setShowInCallButtonGrid(true)
735              .setNumberPresentation(primary.getNumberPresentation())
736              .build());
737    } else if (primaryContactInfo != null) {
738      LogUtil.v(
739          "CallCardPresenter.updatePrimaryDisplayInfo",
740          "update primary display info for " + primaryContactInfo);
741
742      String name = getNameForCall(primaryContactInfo);
743      String number;
744
745      boolean isChildNumberShown = !TextUtils.isEmpty(primary.getChildNumber());
746      boolean isForwardedNumberShown = !TextUtils.isEmpty(primary.getLastForwardedNumber());
747      boolean isCallSubjectShown = shouldShowCallSubject(primary);
748
749      if (isCallSubjectShown) {
750        number = null;
751      } else if (isChildNumberShown) {
752        number = context.getString(R.string.child_number, primary.getChildNumber());
753      } else if (isForwardedNumberShown) {
754        // Use last forwarded number instead of second line, if present.
755        number = primary.getLastForwardedNumber();
756      } else {
757        number = primaryContactInfo.number;
758      }
759
760      boolean nameIsNumber = name != null && name.equals(primaryContactInfo.number);
761
762      // DialerCall with caller that is a work contact.
763      boolean isWorkContact = (primaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
764      inCallScreen.setPrimary(
765          PrimaryInfo.builder()
766              .setNumber(number)
767              .setName(primary.updateNameIfRestricted(name))
768              .setNameIsNumber(nameIsNumber)
769              .setLabel(
770                  shouldShowLocationAsLabel(nameIsNumber, primaryContactInfo.shouldShowLocation)
771                      ? primaryContactInfo.location
772                      : null)
773              .setLocation(
774                  isChildNumberShown || isCallSubjectShown ? null : primaryContactInfo.label)
775              .setPhoto(primaryContactInfo.photo)
776              .setPhotoType(primaryContactInfo.photoType)
777              .setIsSipCall(primaryContactInfo.isSipCall)
778              .setIsContactPhotoShown(showContactPhoto)
779              .setIsWorkCall(hasWorkCallProperty || isWorkContact)
780              .setIsSpam(primary.isSpam())
781              .setIsLocalContact(primaryContactInfo.isLocalContact())
782              .setAnsweringDisconnectsOngoingCall(primary.answeringDisconnectsForegroundVideoCall())
783              .setShouldShowLocation(shouldShowLocation())
784              .setContactInfoLookupKey(primaryContactInfo.lookupKey)
785              .setMultimediaData(multimediaData)
786              .setShowInCallButtonGrid(true)
787              .setNumberPresentation(primary.getNumberPresentation())
788              .build());
789    } else {
790      // Clear the primary display info.
791      inCallScreen.setPrimary(PrimaryInfo.empty());
792    }
793
794    if (isInCallScreenReady) {
795      inCallScreen.showLocationUi(getLocationFragment());
796    } else {
797      LogUtil.i("CallCardPresenter.updatePrimaryDisplayInfo", "UI not ready, not showing location");
798    }
799  }
800
801  private static boolean shouldShowLocationAsLabel(
802      boolean nameIsNumber, boolean shouldShowLocation) {
803    if (nameIsNumber) {
804      return true;
805    }
806    if (shouldShowLocation) {
807      return true;
808    }
809    return false;
810  }
811
812  private Fragment getLocationFragment() {
813    if (!shouldShowLocation()) {
814      return null;
815    }
816    LogUtil.i("CallCardPresenter.getLocationFragment", "returning location fragment");
817    return callLocation.getLocationFragment(context);
818  }
819
820  private boolean shouldShowLocation() {
821    if (!ConfigProviderBindings.get(context)
822        .getBoolean(CONFIG_ENABLE_EMERGENCY_LOCATION, CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT)) {
823      LogUtil.i("CallCardPresenter.getLocationFragment", "disabled by config.");
824      return false;
825    }
826    if (!isPotentialEmergencyCall()) {
827      LogUtil.i("CallCardPresenter.getLocationFragment", "shouldn't show location");
828      return false;
829    }
830    if (!hasLocationPermission()) {
831      LogUtil.i("CallCardPresenter.getLocationFragment", "no location permission.");
832      return false;
833    }
834    if (isBatteryTooLowForEmergencyLocation()) {
835      LogUtil.i("CallCardPresenter.getLocationFragment", "low battery.");
836      return false;
837    }
838    if (ActivityCompat.isInMultiWindowMode(inCallScreen.getInCallScreenFragment().getActivity())) {
839      LogUtil.i("CallCardPresenter.getLocationFragment", "in multi-window mode");
840      return false;
841    }
842    if (primary.isVideoCall()) {
843      LogUtil.i("CallCardPresenter.getLocationFragment", "emergency video calls not supported");
844      return false;
845    }
846    if (!callLocation.canGetLocation(context)) {
847      LogUtil.i("CallCardPresenter.getLocationFragment", "can't get current location");
848      return false;
849    }
850    return true;
851  }
852
853  private boolean isPotentialEmergencyCall() {
854    if (isOutgoingEmergencyCall(primary)) {
855      LogUtil.i("CallCardPresenter.shouldShowLocation", "new emergency call");
856      return true;
857    } else if (isIncomingEmergencyCall(primary)) {
858      LogUtil.i("CallCardPresenter.shouldShowLocation", "potential emergency callback");
859      return true;
860    } else if (isIncomingEmergencyCall(secondary)) {
861      LogUtil.i("CallCardPresenter.shouldShowLocation", "has potential emergency callback");
862      return true;
863    }
864    return false;
865  }
866
867  private static boolean isOutgoingEmergencyCall(@Nullable DialerCall call) {
868    return call != null && !call.isIncoming() && call.isEmergencyCall();
869  }
870
871  private static boolean isIncomingEmergencyCall(@Nullable DialerCall call) {
872    return call != null && call.isIncoming() && call.isPotentialEmergencyCallback();
873  }
874
875  private boolean hasLocationPermission() {
876    return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
877        == PackageManager.PERMISSION_GRANTED;
878  }
879
880  private boolean isBatteryTooLowForEmergencyLocation() {
881    Intent batteryStatus =
882        context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
883    int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
884    if (status == BatteryManager.BATTERY_STATUS_CHARGING
885        || status == BatteryManager.BATTERY_STATUS_FULL) {
886      // Plugged in or full battery
887      return false;
888    }
889    int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
890    int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
891    float batteryPercent = (100f * level) / scale;
892    long threshold =
893        ConfigProviderBindings.get(context)
894            .getLong(
895                CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION,
896                CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT);
897    LogUtil.i(
898        "CallCardPresenter.isBatteryTooLowForEmergencyLocation",
899        "percent charged: " + batteryPercent + ", min required charge: " + threshold);
900    return batteryPercent < threshold;
901  }
902
903  private void updateSecondaryDisplayInfo() {
904    if (inCallScreen == null) {
905      return;
906    }
907
908    if (secondary == null) {
909      // Clear the secondary display info.
910      inCallScreen.setSecondary(SecondaryInfo.builder().setIsFullscreen(isFullscreen).build());
911      return;
912    }
913
914    if (secondary.isMergeInProcess()) {
915      LogUtil.i(
916          "CallCardPresenter.updateSecondaryDisplayInfo",
917          "secondary call is merge in process, clearing info");
918      inCallScreen.setSecondary(SecondaryInfo.builder().setIsFullscreen(isFullscreen).build());
919      return;
920    }
921
922    if (secondary.isConferenceCall()) {
923      inCallScreen.setSecondary(
924          SecondaryInfo.builder()
925              .setShouldShow(true)
926              .setName(
927                  CallerInfoUtils.getConferenceString(
928                      context, secondary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)))
929              .setProviderLabel(secondary.getCallProviderLabel())
930              .setIsConference(true)
931              .setIsVideoCall(secondary.isVideoCall())
932              .setIsFullscreen(isFullscreen)
933              .build());
934    } else if (secondaryContactInfo != null) {
935      LogUtil.v("CallCardPresenter.updateSecondaryDisplayInfo", "" + secondaryContactInfo);
936      String name = getNameForCall(secondaryContactInfo);
937      boolean nameIsNumber = name != null && name.equals(secondaryContactInfo.number);
938      inCallScreen.setSecondary(
939          SecondaryInfo.builder()
940              .setShouldShow(true)
941              .setName(secondary.updateNameIfRestricted(name))
942              .setNameIsNumber(nameIsNumber)
943              .setLabel(secondaryContactInfo.label)
944              .setProviderLabel(secondary.getCallProviderLabel())
945              .setIsVideoCall(secondary.isVideoCall())
946              .setIsFullscreen(isFullscreen)
947              .build());
948    } else {
949      // Clear the secondary display info.
950      inCallScreen.setSecondary(SecondaryInfo.builder().setIsFullscreen(isFullscreen).build());
951    }
952  }
953
954  /** Returns the gateway number for any existing outgoing call. */
955  private String getGatewayNumber() {
956    if (hasOutgoingGatewayCall()) {
957      return DialerCall.getNumberFromHandle(primary.getGatewayInfo().getGatewayAddress());
958    }
959    return null;
960  }
961
962  /**
963   * Returns the label (line of text above the number/name) for any given call. For example,
964   * "calling via [Account/Google Voice]" for outgoing calls.
965   */
966  private String getConnectionLabel() {
967    if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE)
968        != PackageManager.PERMISSION_GRANTED) {
969      return null;
970    }
971    StatusHints statusHints = primary.getStatusHints();
972    if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) {
973      return statusHints.getLabel().toString();
974    }
975
976    if (hasOutgoingGatewayCall() && getUi() != null) {
977      // Return the label for the gateway app on outgoing calls.
978      final PackageManager pm = context.getPackageManager();
979      try {
980        ApplicationInfo info =
981            pm.getApplicationInfo(primary.getGatewayInfo().getGatewayProviderPackageName(), 0);
982        return pm.getApplicationLabel(info).toString();
983      } catch (PackageManager.NameNotFoundException e) {
984        LogUtil.e("CallCardPresenter.getConnectionLabel", "gateway Application Not Found.", e);
985        return null;
986      }
987    }
988    return primary.getCallProviderLabel();
989  }
990
991  private Drawable getCallStateIcon() {
992    // Return connection icon if one exists.
993    StatusHints statusHints = primary.getStatusHints();
994    if (statusHints != null && statusHints.getIcon() != null) {
995      Drawable icon = statusHints.getIcon().loadDrawable(context);
996      if (icon != null) {
997        return icon;
998      }
999    }
1000
1001    return null;
1002  }
1003
1004  private boolean hasOutgoingGatewayCall() {
1005    // We only display the gateway information while STATE_DIALING so return false for any other
1006    // call state.
1007    // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
1008    // is also called after a contact search completes (call is not present yet).  Split the
1009    // UI update so it can receive independent updates.
1010    if (primary == null) {
1011      return false;
1012    }
1013    return DialerCall.State.isDialing(primary.getState())
1014        && primary.getGatewayInfo() != null
1015        && !primary.getGatewayInfo().isEmpty();
1016  }
1017
1018  /** Gets the name to display for the call. */
1019  private String getNameForCall(ContactCacheEntry contactInfo) {
1020    String preferredName =
1021        ContactDisplayUtils.getPreferredDisplayName(
1022            contactInfo.namePrimary, contactInfo.nameAlternative, contactsPreferences);
1023    if (TextUtils.isEmpty(preferredName)) {
1024      return contactInfo.number;
1025    }
1026    return preferredName;
1027  }
1028
1029  @Override
1030  public void onSecondaryInfoClicked() {
1031    if (secondary == null) {
1032      LogUtil.e(
1033          "CallCardPresenter.onSecondaryInfoClicked",
1034          "secondary info clicked but no secondary call.");
1035      return;
1036    }
1037
1038    Logger.get(context)
1039        .logCallImpression(
1040            DialerImpression.Type.IN_CALL_SWAP_SECONDARY_BUTTON_PRESSED,
1041            primary.getUniqueCallId(),
1042            primary.getTimeAddedMs());
1043    LogUtil.i(
1044        "CallCardPresenter.onSecondaryInfoClicked", "swapping call to foreground: " + secondary);
1045    secondary.unhold();
1046  }
1047
1048  @Override
1049  public void onEndCallClicked() {
1050    LogUtil.i("CallCardPresenter.onEndCallClicked", "disconnecting call: " + primary);
1051    if (primary != null) {
1052      primary.disconnect();
1053    }
1054    PostCall.onDisconnectPressed(context);
1055  }
1056
1057  /**
1058   * Handles a change to the fullscreen mode of the in-call UI.
1059   *
1060   * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode.
1061   */
1062  @Override
1063  public void onFullscreenModeChanged(boolean isFullscreenMode) {
1064    isFullscreen = isFullscreenMode;
1065    if (inCallScreen == null) {
1066      return;
1067    }
1068    maybeShowManageConferenceCallButton();
1069  }
1070
1071  private boolean isPrimaryCallActive() {
1072    return primary != null && primary.getState() == DialerCall.State.ACTIVE;
1073  }
1074
1075  private boolean shouldShowEndCallButton(DialerCall primary, int callState) {
1076    if (primary == null) {
1077      return false;
1078    }
1079    if ((!DialerCall.State.isConnectingOrConnected(callState)
1080            && callState != DialerCall.State.DISCONNECTING
1081            && callState != DialerCall.State.DISCONNECTED)
1082        || callState == DialerCall.State.INCOMING) {
1083      return false;
1084    }
1085    if (this.primary.getVideoTech().getSessionModificationState()
1086        == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
1087      return false;
1088    }
1089    return true;
1090  }
1091
1092  @Override
1093  public void onInCallScreenResumed() {
1094    updatePrimaryDisplayInfo();
1095
1096    if (shouldSendAccessibilityEvent) {
1097      handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS);
1098    }
1099  }
1100
1101  @Override
1102  public void onInCallScreenPaused() {}
1103
1104  static boolean sendAccessibilityEvent(Context context, InCallScreen inCallScreen) {
1105    AccessibilityManager am =
1106        (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
1107    if (!am.isEnabled()) {
1108      LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "accessibility is off");
1109      return false;
1110    }
1111    if (inCallScreen == null) {
1112      LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "incallscreen is null");
1113      return false;
1114    }
1115    Fragment fragment = inCallScreen.getInCallScreenFragment();
1116    if (fragment == null || fragment.getView() == null || fragment.getView().getParent() == null) {
1117      LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "fragment/view/parent is null");
1118      return false;
1119    }
1120
1121    DisplayManager displayManager =
1122        (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
1123    Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
1124    boolean screenIsOn = display.getState() == Display.STATE_ON;
1125    LogUtil.d("CallCardPresenter.sendAccessibilityEvent", "screen is on: %b", screenIsOn);
1126    if (!screenIsOn) {
1127      return false;
1128    }
1129
1130    AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT);
1131    inCallScreen.dispatchPopulateAccessibilityEvent(event);
1132    View view = inCallScreen.getInCallScreenFragment().getView();
1133    view.getParent().requestSendAccessibilityEvent(view, event);
1134    return true;
1135  }
1136
1137  private void maybeSendAccessibilityEvent(
1138      InCallState oldState, final InCallState newState, boolean primaryChanged) {
1139    shouldSendAccessibilityEvent = false;
1140    if (context == null) {
1141      return;
1142    }
1143    final AccessibilityManager am =
1144        (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
1145    if (!am.isEnabled()) {
1146      return;
1147    }
1148    // Announce the current call if it's new incoming/outgoing call or primary call is changed
1149    // due to switching calls between two ongoing calls (one is on hold).
1150    if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING)
1151        || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING)
1152        || primaryChanged) {
1153      LogUtil.i(
1154          "CallCardPresenter.maybeSendAccessibilityEvent", "schedule accessibility announcement");
1155      shouldSendAccessibilityEvent = true;
1156      handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS);
1157    }
1158  }
1159
1160  /**
1161   * Determines whether the call subject should be visible on the UI. For the call subject to be
1162   * visible, the call has to be in an incoming or waiting state, and the subject must not be empty.
1163   *
1164   * @param call The call.
1165   * @return {@code true} if the subject should be shown, {@code false} otherwise.
1166   */
1167  private boolean shouldShowCallSubject(DialerCall call) {
1168    if (call == null) {
1169      return false;
1170    }
1171
1172    boolean isIncomingOrWaiting =
1173        primary.getState() == DialerCall.State.INCOMING
1174            || primary.getState() == DialerCall.State.CALL_WAITING;
1175    return isIncomingOrWaiting
1176        && !TextUtils.isEmpty(call.getCallSubject())
1177        && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED
1178        && call.isCallSubjectSupported();
1179  }
1180
1181  /**
1182   * Determines whether the "note sent" toast should be shown. It should be shown for a new outgoing
1183   * call with a subject.
1184   *
1185   * @param call The call
1186   * @return {@code true} if the toast should be shown, {@code false} otherwise.
1187   */
1188  private boolean shouldShowNoteSentToast(DialerCall call) {
1189    return call != null
1190        && hasCallSubject(call)
1191        && (call.getState() == DialerCall.State.DIALING
1192            || call.getState() == DialerCall.State.CONNECTING);
1193  }
1194
1195  private InCallScreen getUi() {
1196    return inCallScreen;
1197  }
1198
1199  /** Callback for contact lookup. */
1200  public static class ContactLookupCallback implements ContactInfoCacheCallback {
1201
1202    private final WeakReference<CallCardPresenter> callCardPresenter;
1203    private final boolean isPrimary;
1204
1205    public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) {
1206      this.callCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter);
1207      this.isPrimary = isPrimary;
1208    }
1209
1210    @Override
1211    public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
1212      CallCardPresenter presenter = callCardPresenter.get();
1213      if (presenter != null) {
1214        presenter.onContactInfoComplete(callId, entry, isPrimary);
1215      }
1216    }
1217
1218    @Override
1219    public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
1220      CallCardPresenter presenter = callCardPresenter.get();
1221      if (presenter != null) {
1222        presenter.onImageLoadComplete(callId, entry);
1223      }
1224    }
1225  }
1226}
1227