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