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