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