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