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