/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.incallui;

import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.os.BatteryManager;
import android.os.Handler;
import android.os.Trace;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.telecom.Call.Details;
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
import android.view.Display;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import com.android.contacts.common.ContactsUtils;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.configprovider.ConfigProviderComponent;
import com.android.dialer.contacts.ContactsComponent;
import com.android.dialer.logging.DialerImpression;
import com.android.dialer.logging.Logger;
import com.android.dialer.multimedia.MultimediaData;
import com.android.dialer.oem.MotorolaUtils;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
import com.android.dialer.postcall.PostCall;
import com.android.dialer.preferredsim.suggestion.SuggestionProvider;
import com.android.incallui.ContactInfoCache.ContactCacheEntry;
import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
import com.android.incallui.InCallPresenter.InCallDetailsListener;
import com.android.incallui.InCallPresenter.InCallEventListener;
import com.android.incallui.InCallPresenter.InCallState;
import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCallListener;
import com.android.incallui.call.state.DialerCallState;
import com.android.incallui.calllocation.CallLocation;
import com.android.incallui.calllocation.CallLocationComponent;
import com.android.incallui.incall.protocol.ContactPhotoType;
import com.android.incallui.incall.protocol.InCallScreen;
import com.android.incallui.incall.protocol.InCallScreenDelegate;
import com.android.incallui.incall.protocol.PrimaryCallState;
import com.android.incallui.incall.protocol.PrimaryCallState.ButtonState;
import com.android.incallui.incall.protocol.PrimaryInfo;
import com.android.incallui.incall.protocol.SecondaryInfo;
import com.android.incallui.videotech.utils.SessionModificationState;
import java.lang.ref.WeakReference;

/**
 * Controller for the Call Card Fragment. This class listens for changes to InCallState and passes
 * it along to the fragment.
 */
public class CallCardPresenter
    implements InCallStateListener,
        IncomingCallListener,
        InCallDetailsListener,
        InCallEventListener,
        InCallScreenDelegate,
        DialerCallListener {

  /**
   * Amount of time to wait before sending an announcement via the accessibility manager. When the
   * call state changes to an outgoing or incoming state for the first time, the UI can often be
   * changing due to call updates or contact lookup. This allows the UI to settle to a stable state
   * to ensure that the correct information is announced.
   */
  private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS = 500;

  /** Flag to allow the user's current location to be shown during emergency calls. */
  private static final String CONFIG_ENABLE_EMERGENCY_LOCATION = "config_enable_emergency_location";

  private static final boolean CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT = true;

  /**
   * Make it possible to not get location during an emergency call if the battery is too low, since
   * doing so could trigger gps and thus potentially cause the phone to die in the middle of the
   * call.
   */
  private static final String CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION =
      "min_battery_percent_for_emergency_location";

  private static final long CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT = 10;

  private final Context context;
  private final Handler handler = new Handler();

  private DialerCall primary;
  private String primaryNumber;
  private DialerCall secondary;
  private String secondaryNumber;
  private ContactCacheEntry primaryContactInfo;
  private ContactCacheEntry secondaryContactInfo;
  private boolean isFullscreen = false;
  private InCallScreen inCallScreen;
  private boolean isInCallScreenReady;
  private boolean shouldSendAccessibilityEvent;

  @NonNull private final CallLocation callLocation;
  private final Runnable sendAccessibilityEventRunnable =
      new Runnable() {
        @Override
        public void run() {
          shouldSendAccessibilityEvent = !sendAccessibilityEvent(context, getUi());
          LogUtil.i(
              "CallCardPresenter.sendAccessibilityEventRunnable",
              "still should send: %b",
              shouldSendAccessibilityEvent);
          if (!shouldSendAccessibilityEvent) {
            handler.removeCallbacks(this);
          }
        }
      };

  public CallCardPresenter(Context context) {
    LogUtil.i("CallCardPresenter.constructor", null);
    this.context = Assert.isNotNull(context).getApplicationContext();
    callLocation = CallLocationComponent.get(this.context).getCallLocation();
  }

  private static boolean hasCallSubject(DialerCall call) {
    return !TextUtils.isEmpty(call.getCallSubject());
  }

  @Override
  public void onInCallScreenDelegateInit(InCallScreen inCallScreen) {
    Assert.isNotNull(inCallScreen);
    this.inCallScreen = inCallScreen;

    // Call may be null if disconnect happened already.
    DialerCall call = CallList.getInstance().getFirstCall();
    if (call != null) {
      primary = call;
      if (shouldShowNoteSentToast(primary)) {
        this.inCallScreen.showNoteSentToast();
      }
      call.addListener(this);
      // start processing lookups right away.
      if (!call.isConferenceCall()) {
        startContactInfoSearch(call, true, call.getState() == DialerCallState.INCOMING);
      } else {
        updateContactEntry(null, true);
      }
    }

    onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance());
  }

  @Override
  public void onInCallScreenReady() {
    LogUtil.i("CallCardPresenter.onInCallScreenReady", null);
    Assert.checkState(!isInCallScreenReady);

    // Contact search may have completed before ui is ready.
    if (primaryContactInfo != null) {
      updatePrimaryDisplayInfo();
    }

    // Register for call state changes last
    InCallPresenter.getInstance().addListener(this);
    InCallPresenter.getInstance().addIncomingCallListener(this);
    InCallPresenter.getInstance().addDetailsListener(this);
    InCallPresenter.getInstance().addInCallEventListener(this);
    isInCallScreenReady = true;

    // Log location impressions
    if (isOutgoingEmergencyCall(primary)) {
      Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_NEW_EMERGENCY_CALL);
    } else if (isIncomingEmergencyCall(primary) || isIncomingEmergencyCall(secondary)) {
      Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_CALLBACK);
    }

    // Showing the location may have been skipped if the UI wasn't ready during previous layout.
    if (shouldShowLocation()) {
      inCallScreen.showLocationUi(getLocationFragment());

      // Log location impressions
      if (!hasLocationPermission()) {
        Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_NO_LOCATION_PERMISSION);
      } else if (isBatteryTooLowForEmergencyLocation()) {
        Logger.get(context)
            .logImpression(DialerImpression.Type.EMERGENCY_BATTERY_TOO_LOW_TO_GET_LOCATION);
      } else if (!callLocation.canGetLocation(context)) {
        Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_CANT_GET_LOCATION);
      }
    }
  }

  @Override
  public void onInCallScreenUnready() {
    LogUtil.i("CallCardPresenter.onInCallScreenUnready", null);
    Assert.checkState(isInCallScreenReady);

    // stop getting call state changes
    InCallPresenter.getInstance().removeListener(this);
    InCallPresenter.getInstance().removeIncomingCallListener(this);
    InCallPresenter.getInstance().removeDetailsListener(this);
    InCallPresenter.getInstance().removeInCallEventListener(this);
    if (primary != null) {
      primary.removeListener(this);
    }

    callLocation.close();

    primary = null;
    primaryContactInfo = null;
    secondaryContactInfo = null;
    isInCallScreenReady = false;
  }

  @Override
  public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
    // same logic should happen as with onStateChange()
    onStateChange(oldState, newState, CallList.getInstance());
  }

  @Override
  public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
    Trace.beginSection("CallCardPresenter.onStateChange");
    LogUtil.v("CallCardPresenter.onStateChange", "oldState: %s, newState: %s", oldState, newState);
    if (inCallScreen == null) {
      Trace.endSection();
      return;
    }

    DialerCall primary = null;
    DialerCall secondary = null;

    if (newState == InCallState.INCOMING) {
      primary = callList.getIncomingCall();
    } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) {
      primary = callList.getOutgoingCall();
      if (primary == null) {
        primary = callList.getPendingOutgoingCall();
      }

      // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
      // highest priority call to display as the secondary call.
      secondary = InCallPresenter.getCallToDisplay(callList, null, true);
    } else if (newState == InCallState.INCALL) {
      primary = InCallPresenter.getCallToDisplay(callList, null, false);
      secondary = InCallPresenter.getCallToDisplay(callList, primary, true);
    }

    LogUtil.v("CallCardPresenter.onStateChange", "primary call: " + primary);
    LogUtil.v("CallCardPresenter.onStateChange", "secondary call: " + secondary);
    String primaryNumber = null;
    String secondaryNumber = null;
    if (primary != null) {
      primaryNumber = primary.getNumber();
    }
    if (secondary != null) {
      secondaryNumber = secondary.getNumber();
    }

    final boolean primaryChanged =
        !(DialerCall.areSame(this.primary, primary)
            && TextUtils.equals(this.primaryNumber, primaryNumber));
    final boolean secondaryChanged =
        !(DialerCall.areSame(this.secondary, secondary)
            && TextUtils.equals(this.secondaryNumber, secondaryNumber));

    this.secondary = secondary;
    this.secondaryNumber = secondaryNumber;
    DialerCall previousPrimary = this.primary;
    this.primary = primary;
    this.primaryNumber = primaryNumber;

    if (this.primary != null) {
      inCallScreen.updateInCallScreenColors();
    }

    if (primaryChanged && shouldShowNoteSentToast(primary)) {
      inCallScreen.showNoteSentToast();
    }

    // Refresh primary call information if either:
    // 1. Primary call changed.
    // 2. The call's ability to manage conference has changed.
    if (shouldRefreshPrimaryInfo(primaryChanged)) {
      // primary call has changed
      if (previousPrimary != null) {
        previousPrimary.removeListener(this);
      }
      this.primary.addListener(this);

      primaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(context, this.primary);
      updatePrimaryDisplayInfo();
      maybeStartSearch(this.primary, true);
    }

    if (previousPrimary != null && this.primary == null) {
      previousPrimary.removeListener(this);
    }

    if (secondaryChanged) {
      if (this.secondary == null) {
        // Secondary call may have ended.  Update the ui.
        secondaryContactInfo = null;
        updateSecondaryDisplayInfo();
      } else {
        // secondary call has changed
        secondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(context, this.secondary);
        updateSecondaryDisplayInfo();
        maybeStartSearch(this.secondary, false);
      }
    }

    // Set the call state
    int callState = DialerCallState.IDLE;
    if (this.primary != null) {
      callState = this.primary.getState();
      updatePrimaryCallState();
    } else {
      getUi().setCallState(PrimaryCallState.empty());
    }

    maybeShowManageConferenceCallButton();

    // Hide the end call button instantly if we're receiving an incoming call.
    getUi()
        .setEndCallButtonEnabled(
            shouldShowEndCallButton(this.primary, callState),
            callState != DialerCallState.INCOMING /* animate */);

    maybeSendAccessibilityEvent(oldState, newState, primaryChanged);
    Trace.endSection();
  }

  @Override
  public void onDetailsChanged(DialerCall call, Details details) {
    updatePrimaryCallState();

    if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE)
        != details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) {
      maybeShowManageConferenceCallButton();
    }
  }

  @Override
  public void onDialerCallDisconnect() {}

  @Override
  public void onDialerCallUpdate() {
    // No-op; specific call updates handled elsewhere.
  }

  @Override
  public void onWiFiToLteHandover() {}

  @Override
  public void onHandoverToWifiFailure() {}

  @Override
  public void onInternationalCallOnWifi() {}

  @Override
  public void onEnrichedCallSessionUpdate() {
    LogUtil.enterBlock("CallCardPresenter.onEnrichedCallSessionUpdate");
    updatePrimaryDisplayInfo();
  }

  /** Handles a change to the child number by refreshing the primary call info. */
  @Override
  public void onDialerCallChildNumberChange() {
    LogUtil.v("CallCardPresenter.onDialerCallChildNumberChange", "");

    if (primary == null) {
      return;
    }
    updatePrimaryDisplayInfo();
  }

  /** Handles a change to the last forwarding number by refreshing the primary call info. */
  @Override
  public void onDialerCallLastForwardedNumberChange() {
    LogUtil.v("CallCardPresenter.onDialerCallLastForwardedNumberChange", "");

    if (primary == null) {
      return;
    }
    updatePrimaryDisplayInfo();
    updatePrimaryCallState();
  }

  @Override
  public void onDialerCallUpgradeToVideo() {}

  /** Handles a change to the session modification state for a call. */
  @Override
  public void onDialerCallSessionModificationStateChange() {
    LogUtil.enterBlock("CallCardPresenter.onDialerCallSessionModificationStateChange");

    if (primary == null) {
      return;
    }
    getUi()
        .setEndCallButtonEnabled(
            primary.getVideoTech().getSessionModificationState()
                != SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
            true /* shouldAnimate */);
    updatePrimaryCallState();
  }

  private boolean shouldRefreshPrimaryInfo(boolean primaryChanged) {
    if (primary == null) {
      return false;
    }
    return primaryChanged
        || inCallScreen.isManageConferenceVisible() != shouldShowManageConference();
  }

  private void updatePrimaryCallState() {
    if (getUi() != null && primary != null) {
      boolean isWorkCall =
          primary.hasProperty(PROPERTY_ENTERPRISE_CALL)
              || (primaryContactInfo != null
                  && primaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
      boolean isHdAudioCall =
          isPrimaryCallActive() && primary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO);
      boolean isAttemptingHdAudioCall =
          !isHdAudioCall
              && !primary.hasProperty(DialerCall.PROPERTY_CODEC_KNOWN)
              && MotorolaUtils.shouldBlinkHdIconWhenConnectingCall(context);

      boolean isBusiness = primaryContactInfo != null && primaryContactInfo.isBusiness;

      // Check for video state change and update the visibility of the contact photo.  The contact
      // photo is hidden when the incoming video surface is shown.
      // The contact photo visibility can also change in setPrimary().
      boolean shouldShowContactPhoto =
          !VideoCallPresenter.showIncomingVideo(primary.getVideoState(), primary.getState());
      getUi()
          .setCallState(
              PrimaryCallState.builder()
                  .setState(primary.getState())
                  .setIsVideoCall(primary.isVideoCall())
                  .setSessionModificationState(primary.getVideoTech().getSessionModificationState())
                  .setDisconnectCause(primary.getDisconnectCause())
                  .setConnectionLabel(getConnectionLabel())
                  .setPrimaryColor(
                      InCallPresenter.getInstance().getThemeColorManager().getPrimaryColor())
                  .setSimSuggestionReason(getSimSuggestionReason())
                  .setConnectionIcon(getCallStateIcon())
                  .setGatewayNumber(getGatewayNumber())
                  .setCallSubject(shouldShowCallSubject(primary) ? primary.getCallSubject() : null)
                  .setCallbackNumber(
                      PhoneNumberHelper.formatNumber(
                          context, primary.getCallbackNumber(), primary.getSimCountryIso()))
                  .setIsWifi(primary.hasProperty(Details.PROPERTY_WIFI))
                  .setIsConference(
                      primary.isConferenceCall()
                          && !primary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE))
                  .setIsWorkCall(isWorkCall)
                  .setIsHdAttempting(isAttemptingHdAudioCall)
                  .setIsHdAudioCall(isHdAudioCall)
                  .setIsForwardedNumber(
                      !TextUtils.isEmpty(primary.getLastForwardedNumber())
                          || primary.isCallForwarded())
                  .setShouldShowContactPhoto(shouldShowContactPhoto)
                  .setConnectTimeMillis(primary.getConnectTimeMillis())
                  .setIsVoiceMailNumber(primary.isVoiceMailNumber())
                  .setIsRemotelyHeld(primary.isRemotelyHeld())
                  .setIsBusinessNumber(isBusiness)
                  .setSupportsCallOnHold(supports2ndCallOnHold())
                  .setSwapToSecondaryButtonState(getSwapToSecondaryButtonState())
                  .setIsAssistedDialed(primary.isAssistedDialed())
                  .setCustomLabel(null)
                  .setAssistedDialingExtras(primary.getAssistedDialingExtras())
                  .build());

      InCallActivity activity =
          (InCallActivity) (inCallScreen.getInCallScreenFragment().getActivity());
      if (activity != null) {
        activity.onPrimaryCallStateChanged();
      }
    }
  }

  private @ButtonState int getSwapToSecondaryButtonState() {
    if (secondary == null) {
      return ButtonState.NOT_SUPPORT;
    }
    if (primary.getState() == DialerCallState.ACTIVE) {
      return ButtonState.ENABLED;
    }
    return ButtonState.DISABLED;
  }

  /** Only show the conference call button if we can manage the conference. */
  private void maybeShowManageConferenceCallButton() {
    getUi().showManageConferenceCallButton(shouldShowManageConference());
  }

  /**
   * Determines if the manage conference button should be visible, based on the current primary
   * call.
   *
   * @return {@code True} if the manage conference button should be visible.
   */
  private boolean shouldShowManageConference() {
    if (primary == null) {
      return false;
    }

    return primary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE) && !isFullscreen;
  }

  private boolean supports2ndCallOnHold() {
    DialerCall firstCall = CallList.getInstance().getActiveOrBackgroundCall();
    DialerCall incomingCall = CallList.getInstance().getIncomingCall();
    if (firstCall != null && incomingCall != null && firstCall != incomingCall) {
      return incomingCall.can(Details.CAPABILITY_HOLD);
    }
    return true;
  }

  @Override
  public void onCallStateButtonClicked() {
    Intent broadcastIntent = Bindings.get(context).getCallStateButtonBroadcastIntent(context);
    if (broadcastIntent != null) {
      LogUtil.v(
          "CallCardPresenter.onCallStateButtonClicked",
          "sending call state button broadcast: " + broadcastIntent);
      context.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE);
    }
  }

  @Override
  public void onManageConferenceClicked() {
    InCallActivity activity =
        (InCallActivity) (inCallScreen.getInCallScreenFragment().getActivity());
    activity.showConferenceFragment(true);
  }

  @Override
  public void onShrinkAnimationComplete() {
    InCallPresenter.getInstance().onShrinkAnimationComplete();
  }

  private void maybeStartSearch(DialerCall call, boolean isPrimary) {
    // no need to start search for conference calls which show generic info.
    if (call != null && !call.isConferenceCall()) {
      startContactInfoSearch(call, isPrimary, call.getState() == DialerCallState.INCOMING);
    }
  }

  /** Starts a query for more contact data for the save primary and secondary calls. */
  private void startContactInfoSearch(
      final DialerCall call, final boolean isPrimary, boolean isIncoming) {
    final ContactInfoCache cache = ContactInfoCache.getInstance(context);

    cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary));
  }

  private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) {
    final boolean entryMatchesExistingCall =
        (isPrimary && primary != null && TextUtils.equals(callId, primary.getId()))
            || (!isPrimary && secondary != null && TextUtils.equals(callId, secondary.getId()));
    if (entryMatchesExistingCall) {
      updateContactEntry(entry, isPrimary);
    } else {
      LogUtil.e(
          "CallCardPresenter.onContactInfoComplete",
          "dropping stale contact lookup info for " + callId);
    }

    final DialerCall call = CallList.getInstance().getCallById(callId);
    if (call != null) {
      call.getLogState().contactLookupResult = entry.contactLookupResult;
    }
    if (entry.lookupUri != null) {
      CallerInfoUtils.sendViewNotification(context, entry.lookupUri);
    }
  }

  private void onImageLoadComplete(String callId, ContactCacheEntry entry) {
    if (getUi() == null) {
      return;
    }

    if (entry.photo != null) {
      if (primary != null && callId.equals(primary.getId())) {
        updateContactEntry(entry, true /* isPrimary */);
      } else if (secondary != null && callId.equals(secondary.getId())) {
        updateContactEntry(entry, false /* isPrimary */);
      }
    }
  }

  private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) {
    if (isPrimary) {
      primaryContactInfo = entry;
      updatePrimaryDisplayInfo();
    } else {
      secondaryContactInfo = entry;
      updateSecondaryDisplayInfo();
    }
  }

  private void updatePrimaryDisplayInfo() {
    if (inCallScreen == null) {
      // TODO: May also occur if search result comes back after ui is destroyed. Look into
      // removing that case completely.
      LogUtil.v(
          "CallCardPresenter.updatePrimaryDisplayInfo",
          "updatePrimaryDisplayInfo called but ui is null!");
      return;
    }

    if (primary == null) {
      // Clear the primary display info.
      inCallScreen.setPrimary(PrimaryInfo.empty());
      return;
    }

    // Hide the contact photo if we are in a video call and the incoming video surface is
    // showing.
    boolean showContactPhoto =
        !VideoCallPresenter.showIncomingVideo(primary.getVideoState(), primary.getState());

    // DialerCall placed through a work phone account.
    boolean hasWorkCallProperty = primary.hasProperty(PROPERTY_ENTERPRISE_CALL);

    MultimediaData multimediaData = null;
    if (primary.getEnrichedCallSession() != null) {
      multimediaData = primary.getEnrichedCallSession().getMultimediaData();
    }

    if (primary.isConferenceCall()) {
      LogUtil.v(
          "CallCardPresenter.updatePrimaryDisplayInfo",
          "update primary display info for conference call.");

      inCallScreen.setPrimary(
          PrimaryInfo.builder()
              .setName(
                  CallerInfoUtils.getConferenceString(
                      context, primary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)))
              .setNameIsNumber(false)
              .setPhotoType(ContactPhotoType.DEFAULT_PLACEHOLDER)
              .setIsSipCall(false)
              .setIsContactPhotoShown(showContactPhoto)
              .setIsWorkCall(hasWorkCallProperty)
              .setIsSpam(false)
              .setIsLocalContact(false)
              .setAnsweringDisconnectsOngoingCall(false)
              .setShouldShowLocation(shouldShowLocation())
              .setShowInCallButtonGrid(true)
              .setNumberPresentation(primary.getNumberPresentation())
              .build());
    } else if (primaryContactInfo != null) {
      LogUtil.v(
          "CallCardPresenter.updatePrimaryDisplayInfo",
          "update primary display info for " + primaryContactInfo);

      String name = getNameForCall(primaryContactInfo);
      String number;

      boolean isChildNumberShown = !TextUtils.isEmpty(primary.getChildNumber());
      boolean isForwardedNumberShown = !TextUtils.isEmpty(primary.getLastForwardedNumber());
      boolean isCallSubjectShown = shouldShowCallSubject(primary);

      if (isCallSubjectShown) {
        number = null;
      } else if (isChildNumberShown) {
        number = context.getString(R.string.child_number, primary.getChildNumber());
      } else if (isForwardedNumberShown) {
        // Use last forwarded number instead of second line, if present.
        number = primary.getLastForwardedNumber();
      } else {
        number = primaryContactInfo.number;
      }

      boolean nameIsNumber = name != null && name.equals(primaryContactInfo.number);

      // DialerCall with caller that is a work contact.
      boolean isWorkContact = (primaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
      inCallScreen.setPrimary(
          PrimaryInfo.builder()
              .setNumber(number)
              .setName(primary.updateNameIfRestricted(name))
              .setNameIsNumber(nameIsNumber)
              .setLocation(
                  shouldShowLocationAsLabel(nameIsNumber, primaryContactInfo.shouldShowLocation)
                      ? primaryContactInfo.location
                      : null)
              .setLabel(isChildNumberShown || isCallSubjectShown ? null : primaryContactInfo.label)
              .setPhoto(primaryContactInfo.photo)
              .setPhotoUri(primaryContactInfo.displayPhotoUri)
              .setPhotoType(primaryContactInfo.photoType)
              .setIsSipCall(primaryContactInfo.isSipCall)
              .setIsContactPhotoShown(showContactPhoto)
              .setIsWorkCall(hasWorkCallProperty || isWorkContact)
              .setIsSpam(primary.isSpam())
              .setIsLocalContact(primaryContactInfo.isLocalContact())
              .setAnsweringDisconnectsOngoingCall(primary.answeringDisconnectsForegroundVideoCall())
              .setShouldShowLocation(shouldShowLocation())
              .setContactInfoLookupKey(primaryContactInfo.lookupKey)
              .setMultimediaData(multimediaData)
              .setShowInCallButtonGrid(true)
              .setNumberPresentation(primary.getNumberPresentation())
              .build());
    } else {
      // Clear the primary display info.
      inCallScreen.setPrimary(PrimaryInfo.empty());
    }

    if (isInCallScreenReady) {
      inCallScreen.showLocationUi(getLocationFragment());
    } else {
      LogUtil.i("CallCardPresenter.updatePrimaryDisplayInfo", "UI not ready, not showing location");
    }
  }

  private static boolean shouldShowLocationAsLabel(
      boolean nameIsNumber, boolean shouldShowLocation) {
    if (nameIsNumber) {
      return true;
    }
    if (shouldShowLocation) {
      return true;
    }
    return false;
  }

  private Fragment getLocationFragment() {
    if (!shouldShowLocation()) {
      return null;
    }
    LogUtil.i("CallCardPresenter.getLocationFragment", "returning location fragment");
    return callLocation.getLocationFragment(context);
  }

  private boolean shouldShowLocation() {
    if (!ConfigProviderComponent.get(context)
        .getConfigProvider()
        .getBoolean(CONFIG_ENABLE_EMERGENCY_LOCATION, CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT)) {
      LogUtil.i("CallCardPresenter.getLocationFragment", "disabled by config.");
      return false;
    }
    if (!isPotentialEmergencyCall()) {
      LogUtil.i("CallCardPresenter.getLocationFragment", "shouldn't show location");
      return false;
    }
    if (!hasLocationPermission()) {
      LogUtil.i("CallCardPresenter.getLocationFragment", "no location permission.");
      return false;
    }
    if (isBatteryTooLowForEmergencyLocation()) {
      LogUtil.i("CallCardPresenter.getLocationFragment", "low battery.");
      return false;
    }
    if (inCallScreen.getInCallScreenFragment().getActivity().isInMultiWindowMode()) {
      LogUtil.i("CallCardPresenter.getLocationFragment", "in multi-window mode");
      return false;
    }
    if (primary.isVideoCall()) {
      LogUtil.i("CallCardPresenter.getLocationFragment", "emergency video calls not supported");
      return false;
    }
    if (!callLocation.canGetLocation(context)) {
      LogUtil.i("CallCardPresenter.getLocationFragment", "can't get current location");
      return false;
    }
    return true;
  }

  private boolean isPotentialEmergencyCall() {
    if (isOutgoingEmergencyCall(primary)) {
      LogUtil.i("CallCardPresenter.shouldShowLocation", "new emergency call");
      return true;
    } else if (isIncomingEmergencyCall(primary)) {
      LogUtil.i("CallCardPresenter.shouldShowLocation", "potential emergency callback");
      return true;
    } else if (isIncomingEmergencyCall(secondary)) {
      LogUtil.i("CallCardPresenter.shouldShowLocation", "has potential emergency callback");
      return true;
    }
    return false;
  }

  private static boolean isOutgoingEmergencyCall(@Nullable DialerCall call) {
    return call != null && !call.isIncoming() && call.isEmergencyCall();
  }

  private static boolean isIncomingEmergencyCall(@Nullable DialerCall call) {
    return call != null && call.isIncoming() && call.isPotentialEmergencyCallback();
  }

  private boolean hasLocationPermission() {
    return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
        == PackageManager.PERMISSION_GRANTED;
  }

  private boolean isBatteryTooLowForEmergencyLocation() {
    Intent batteryStatus =
        context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
    int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
    if (status == BatteryManager.BATTERY_STATUS_CHARGING
        || status == BatteryManager.BATTERY_STATUS_FULL) {
      // Plugged in or full battery
      return false;
    }
    int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
    int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
    float batteryPercent = (100f * level) / scale;
    long threshold =
        ConfigProviderComponent.get(context)
            .getConfigProvider()
            .getLong(
                CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION,
                CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT);
    LogUtil.i(
        "CallCardPresenter.isBatteryTooLowForEmergencyLocation",
        "percent charged: " + batteryPercent + ", min required charge: " + threshold);
    return batteryPercent < threshold;
  }

  private void updateSecondaryDisplayInfo() {
    if (inCallScreen == null) {
      return;
    }

    if (secondary == null) {
      // Clear the secondary display info.
      inCallScreen.setSecondary(SecondaryInfo.builder().setIsFullscreen(isFullscreen).build());
      return;
    }

    if (secondary.isMergeInProcess()) {
      LogUtil.i(
          "CallCardPresenter.updateSecondaryDisplayInfo",
          "secondary call is merge in process, clearing info");
      inCallScreen.setSecondary(SecondaryInfo.builder().setIsFullscreen(isFullscreen).build());
      return;
    }

    if (secondary.isConferenceCall()) {
      inCallScreen.setSecondary(
          SecondaryInfo.builder()
              .setShouldShow(true)
              .setName(
                  CallerInfoUtils.getConferenceString(
                      context, secondary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)))
              .setProviderLabel(secondary.getCallProviderLabel())
              .setIsConference(true)
              .setIsVideoCall(secondary.isVideoCall())
              .setIsFullscreen(isFullscreen)
              .build());
    } else if (secondaryContactInfo != null) {
      LogUtil.v("CallCardPresenter.updateSecondaryDisplayInfo", "" + secondaryContactInfo);
      String name = getNameForCall(secondaryContactInfo);
      boolean nameIsNumber = name != null && name.equals(secondaryContactInfo.number);
      inCallScreen.setSecondary(
          SecondaryInfo.builder()
              .setShouldShow(true)
              .setName(secondary.updateNameIfRestricted(name))
              .setNameIsNumber(nameIsNumber)
              .setLabel(secondaryContactInfo.label)
              .setProviderLabel(secondary.getCallProviderLabel())
              .setIsVideoCall(secondary.isVideoCall())
              .setIsFullscreen(isFullscreen)
              .build());
    } else {
      // Clear the secondary display info.
      inCallScreen.setSecondary(SecondaryInfo.builder().setIsFullscreen(isFullscreen).build());
    }
  }

  /** Returns the gateway number for any existing outgoing call. */
  private String getGatewayNumber() {
    if (hasOutgoingGatewayCall()) {
      return DialerCall.getNumberFromHandle(primary.getGatewayInfo().getGatewayAddress());
    }
    return null;
  }

  /**
   * Returns the label (line of text above the number/name) for any given call. For example,
   * "calling via [Account/Google Voice]" for outgoing calls.
   */
  private String getConnectionLabel() {
    if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE)
        != PackageManager.PERMISSION_GRANTED) {
      return null;
    }
    StatusHints statusHints = primary.getStatusHints();
    if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) {
      return statusHints.getLabel().toString();
    }

    if (hasOutgoingGatewayCall() && getUi() != null) {
      // Return the label for the gateway app on outgoing calls.
      final PackageManager pm = context.getPackageManager();
      try {
        ApplicationInfo info =
            pm.getApplicationInfo(primary.getGatewayInfo().getGatewayProviderPackageName(), 0);
        return pm.getApplicationLabel(info).toString();
      } catch (PackageManager.NameNotFoundException e) {
        LogUtil.e("CallCardPresenter.getConnectionLabel", "gateway Application Not Found.", e);
        return null;
      }
    }
    return primary.getCallProviderLabel();
  }

  @Nullable
  private SuggestionProvider.Reason getSimSuggestionReason() {
    String value =
        primary.getIntentExtras().getString(SuggestionProvider.EXTRA_SIM_SUGGESTION_REASON);
    if (value == null) {
      return null;
    }
    try {
      return SuggestionProvider.Reason.valueOf(value);
    } catch (IllegalArgumentException e) {
      LogUtil.e("CallCardPresenter.getConnectionLabel", "unknown reason " + value);
      return null;
    }
  }

  private Drawable getCallStateIcon() {
    // Return connection icon if one exists.
    StatusHints statusHints = primary.getStatusHints();
    if (statusHints != null && statusHints.getIcon() != null) {
      Drawable icon = statusHints.getIcon().loadDrawable(context);
      if (icon != null) {
        return icon;
      }
    }

    return null;
  }

  private boolean hasOutgoingGatewayCall() {
    // We only display the gateway information while STATE_DIALING so return false for any other
    // call state.
    // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
    // is also called after a contact search completes (call is not present yet).  Split the
    // UI update so it can receive independent updates.
    if (primary == null) {
      return false;
    }
    return DialerCallState.isDialing(primary.getState())
        && primary.getGatewayInfo() != null
        && !primary.getGatewayInfo().isEmpty();
  }

  /** Gets the name to display for the call. */
  private String getNameForCall(ContactCacheEntry contactInfo) {
    String preferredName =
        ContactsComponent.get(context)
            .contactDisplayPreferences()
            .getDisplayName(contactInfo.namePrimary, contactInfo.nameAlternative);
    if (TextUtils.isEmpty(preferredName)) {
      return TextUtils.isEmpty(contactInfo.number)
          ? null
          : BidiFormatter.getInstance()
              .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
    }
    return preferredName;
  }

  @Override
  public void onSecondaryInfoClicked() {
    if (secondary == null) {
      LogUtil.e(
          "CallCardPresenter.onSecondaryInfoClicked",
          "secondary info clicked but no secondary call.");
      return;
    }

    Logger.get(context)
        .logCallImpression(
            DialerImpression.Type.IN_CALL_SWAP_SECONDARY_BUTTON_PRESSED,
            primary.getUniqueCallId(),
            primary.getTimeAddedMs());
    LogUtil.i(
        "CallCardPresenter.onSecondaryInfoClicked", "swapping call to foreground: " + secondary);
    secondary.unhold();
  }

  @Override
  public void onEndCallClicked() {
    LogUtil.i("CallCardPresenter.onEndCallClicked", "disconnecting call: " + primary);
    if (primary != null) {
      primary.disconnect();
    }
    PostCall.onDisconnectPressed(context);
  }

  /**
   * Handles a change to the fullscreen mode of the in-call UI.
   *
   * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode.
   */
  @Override
  public void onFullscreenModeChanged(boolean isFullscreenMode) {
    isFullscreen = isFullscreenMode;
    if (inCallScreen == null) {
      return;
    }
    maybeShowManageConferenceCallButton();
  }

  private boolean isPrimaryCallActive() {
    return primary != null && primary.getState() == DialerCallState.ACTIVE;
  }

  private boolean shouldShowEndCallButton(DialerCall primary, int callState) {
    if (primary == null) {
      return false;
    }
    if ((!DialerCallState.isConnectingOrConnected(callState)
            && callState != DialerCallState.DISCONNECTING
            && callState != DialerCallState.DISCONNECTED)
        || callState == DialerCallState.INCOMING) {
      return false;
    }
    if (this.primary.getVideoTech().getSessionModificationState()
        == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
      return false;
    }
    return true;
  }

  @Override
  public void onInCallScreenResumed() {
    updatePrimaryDisplayInfo();

    if (shouldSendAccessibilityEvent) {
      handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS);
    }
  }

  @Override
  public void onInCallScreenPaused() {}

  static boolean sendAccessibilityEvent(Context context, InCallScreen inCallScreen) {
    AccessibilityManager am =
        (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
    if (!am.isEnabled()) {
      LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "accessibility is off");
      return false;
    }
    if (inCallScreen == null) {
      LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "incallscreen is null");
      return false;
    }
    Fragment fragment = inCallScreen.getInCallScreenFragment();
    if (fragment == null || fragment.getView() == null || fragment.getView().getParent() == null) {
      LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "fragment/view/parent is null");
      return false;
    }

    DisplayManager displayManager =
        (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
    Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
    boolean screenIsOn = display.getState() == Display.STATE_ON;
    LogUtil.d("CallCardPresenter.sendAccessibilityEvent", "screen is on: %b", screenIsOn);
    if (!screenIsOn) {
      return false;
    }

    AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT);
    inCallScreen.dispatchPopulateAccessibilityEvent(event);
    View view = inCallScreen.getInCallScreenFragment().getView();
    view.getParent().requestSendAccessibilityEvent(view, event);
    return true;
  }

  private void maybeSendAccessibilityEvent(
      InCallState oldState, final InCallState newState, boolean primaryChanged) {
    shouldSendAccessibilityEvent = false;
    if (context == null) {
      return;
    }
    final AccessibilityManager am =
        (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
    if (!am.isEnabled()) {
      return;
    }
    // Announce the current call if it's new incoming/outgoing call or primary call is changed
    // due to switching calls between two ongoing calls (one is on hold).
    if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING)
        || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING)
        || primaryChanged) {
      LogUtil.i(
          "CallCardPresenter.maybeSendAccessibilityEvent", "schedule accessibility announcement");
      shouldSendAccessibilityEvent = true;
      handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS);
    }
  }

  /**
   * Determines whether the call subject should be visible on the UI. For the call subject to be
   * visible, the call has to be in an incoming or waiting state, and the subject must not be empty.
   *
   * @param call The call.
   * @return {@code true} if the subject should be shown, {@code false} otherwise.
   */
  private boolean shouldShowCallSubject(DialerCall call) {
    if (call == null) {
      return false;
    }

    boolean isIncomingOrWaiting =
        primary.getState() == DialerCallState.INCOMING
            || primary.getState() == DialerCallState.CALL_WAITING;
    return isIncomingOrWaiting
        && !TextUtils.isEmpty(call.getCallSubject())
        && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED
        && call.isCallSubjectSupported();
  }

  /**
   * Determines whether the "note sent" toast should be shown. It should be shown for a new outgoing
   * call with a subject.
   *
   * @param call The call
   * @return {@code true} if the toast should be shown, {@code false} otherwise.
   */
  private boolean shouldShowNoteSentToast(DialerCall call) {
    return call != null
        && hasCallSubject(call)
        && (call.getState() == DialerCallState.DIALING
            || call.getState() == DialerCallState.CONNECTING);
  }

  private InCallScreen getUi() {
    return inCallScreen;
  }

  /** Callback for contact lookup. */
  public static class ContactLookupCallback implements ContactInfoCacheCallback {

    private final WeakReference<CallCardPresenter> callCardPresenter;
    private final boolean isPrimary;

    public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) {
      this.callCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter);
      this.isPrimary = isPrimary;
    }

    @Override
    public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
      CallCardPresenter presenter = callCardPresenter.get();
      if (presenter != null) {
        presenter.onContactInfoComplete(callId, entry, isPrimary);
      }
    }

    @Override
    public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
      CallCardPresenter presenter = callCardPresenter.get();
      if (presenter != null) {
        presenter.onImageLoadComplete(callId, entry);
      }
    }
  }
}