• 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 android.Manifest;
20 import android.app.Activity;
21 import android.app.FragmentManager;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.PackageManager;
26 import android.graphics.drawable.Drawable;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.telecom.Call.Details;
30 import android.telecom.DisconnectCause;
31 import android.telecom.PhoneAccount;
32 import android.telecom.PhoneAccountHandle;
33 import android.telecom.StatusHints;
34 import android.telecom.TelecomManager;
35 import android.telecom.VideoProfile;
36 import android.telephony.PhoneNumberUtils;
37 import android.text.TextUtils;
38 import android.view.accessibility.AccessibilityManager;
39 
40 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
41 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
42 import com.android.incallui.InCallPresenter.InCallDetailsListener;
43 import com.android.incallui.InCallPresenter.InCallEventListener;
44 import com.android.incallui.InCallPresenter.InCallState;
45 import com.android.incallui.InCallPresenter.InCallStateListener;
46 import com.android.incallui.InCallPresenter.IncomingCallListener;
47 import com.android.incalluibind.ObjectFactory;
48 
49 import java.lang.ref.WeakReference;
50 
51 import com.google.common.base.Preconditions;
52 
53 /**
54  * Presenter for the Call Card Fragment.
55  * <p>
56  * This class listens for changes to InCallState and passes it along to the fragment.
57  */
58 public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
59         implements InCallStateListener, IncomingCallListener, InCallDetailsListener,
60         InCallEventListener, CallList.CallUpdateListener {
61 
62     public interface EmergencyCallListener {
onCallUpdated(BaseFragment fragment, boolean isEmergency)63         public void onCallUpdated(BaseFragment fragment, boolean isEmergency);
64     }
65 
66     private static final String TAG = CallCardPresenter.class.getSimpleName();
67     private static final long CALL_TIME_UPDATE_INTERVAL_MS = 1000;
68 
69     private final EmergencyCallListener mEmergencyCallListener =
70             ObjectFactory.newEmergencyCallListener();
71 
72     private Call mPrimary;
73     private Call mSecondary;
74     private ContactCacheEntry mPrimaryContactInfo;
75     private ContactCacheEntry mSecondaryContactInfo;
76     private CallTimer mCallTimer;
77     private Context mContext;
78     private boolean mSpinnerShowing = false;
79     private boolean mHasShownToast = false;
80 
81     public static class ContactLookupCallback implements ContactInfoCacheCallback {
82         private final WeakReference<CallCardPresenter> mCallCardPresenter;
83         private final boolean mIsPrimary;
84 
ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary)85         public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) {
86             mCallCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter);
87             mIsPrimary = isPrimary;
88         }
89 
90         @Override
onContactInfoComplete(String callId, ContactCacheEntry entry)91         public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
92             CallCardPresenter presenter = mCallCardPresenter.get();
93             if (presenter != null) {
94                 presenter.onContactInfoComplete(callId, entry, mIsPrimary);
95             }
96         }
97 
98         @Override
onImageLoadComplete(String callId, ContactCacheEntry entry)99         public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
100             CallCardPresenter presenter = mCallCardPresenter.get();
101             if (presenter != null) {
102                 presenter.onImageLoadComplete(callId, entry);
103             }
104         }
105 
106     }
107 
CallCardPresenter()108     public CallCardPresenter() {
109         // create the call timer
110         mCallTimer = new CallTimer(new Runnable() {
111             @Override
112             public void run() {
113                 updateCallTime();
114             }
115         });
116     }
117 
init(Context context, Call call)118     public void init(Context context, Call call) {
119         mContext = Preconditions.checkNotNull(context);
120 
121         // Call may be null if disconnect happened already.
122         if (call != null) {
123             mPrimary = call;
124             if (shouldShowNoteSentToast(mPrimary)) {
125                 final CallCardUi ui = getUi();
126                 if (ui != null) {
127                     ui.showNoteSentToast();
128                 }
129             }
130             CallList.getInstance().addCallUpdateListener(call.getId(), this);
131 
132             // start processing lookups right away.
133             if (!call.isConferenceCall()) {
134                 startContactInfoSearch(call, true, call.getState() == Call.State.INCOMING);
135             } else {
136                 updateContactEntry(null, true);
137             }
138         }
139 
140         onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance());
141     }
142 
143     @Override
onUiReady(CallCardUi ui)144     public void onUiReady(CallCardUi ui) {
145         super.onUiReady(ui);
146 
147         // Contact search may have completed before ui is ready.
148         if (mPrimaryContactInfo != null) {
149             updatePrimaryDisplayInfo();
150         }
151 
152         // Register for call state changes last
153         InCallPresenter.getInstance().addListener(this);
154         InCallPresenter.getInstance().addIncomingCallListener(this);
155         InCallPresenter.getInstance().addDetailsListener(this);
156         InCallPresenter.getInstance().addInCallEventListener(this);
157     }
158 
159     @Override
onUiUnready(CallCardUi ui)160     public void onUiUnready(CallCardUi ui) {
161         super.onUiUnready(ui);
162 
163         // stop getting call state changes
164         InCallPresenter.getInstance().removeListener(this);
165         InCallPresenter.getInstance().removeIncomingCallListener(this);
166         InCallPresenter.getInstance().removeDetailsListener(this);
167         InCallPresenter.getInstance().removeInCallEventListener(this);
168         if (mPrimary != null) {
169             CallList.getInstance().removeCallUpdateListener(mPrimary.getId(), this);
170         }
171 
172         mPrimary = null;
173         mPrimaryContactInfo = null;
174         mSecondaryContactInfo = null;
175     }
176 
177     @Override
onIncomingCall(InCallState oldState, InCallState newState, Call call)178     public void onIncomingCall(InCallState oldState, InCallState newState, Call call) {
179         // same logic should happen as with onStateChange()
180         onStateChange(oldState, newState, CallList.getInstance());
181     }
182 
183     @Override
onStateChange(InCallState oldState, InCallState newState, CallList callList)184     public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
185         Log.d(this, "onStateChange() " + newState);
186         final CallCardUi ui = getUi();
187         if (ui == null) {
188             return;
189         }
190 
191         Call primary = null;
192         Call secondary = null;
193 
194         if (newState == InCallState.INCOMING) {
195             primary = callList.getIncomingCall();
196         } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) {
197             primary = callList.getOutgoingCall();
198             if (primary == null) {
199                 primary = callList.getPendingOutgoingCall();
200             }
201 
202             // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
203             // highest priority call to display as the secondary call.
204             secondary = getCallToDisplay(callList, null, true);
205         } else if (newState == InCallState.INCALL) {
206             primary = getCallToDisplay(callList, null, false);
207             secondary = getCallToDisplay(callList, primary, true);
208         }
209 
210         Log.d(this, "Primary call: " + primary);
211         Log.d(this, "Secondary call: " + secondary);
212 
213         final boolean primaryChanged = !(Call.areSame(mPrimary, primary) &&
214                 Call.areSameNumber(mPrimary, primary));
215         final boolean secondaryChanged = !(Call.areSame(mSecondary, secondary) &&
216                 Call.areSameNumber(mSecondary, secondary));
217         final boolean shouldShowCallSubject = shouldShowCallSubject(mPrimary);
218 
219         mSecondary = secondary;
220         Call previousPrimary = mPrimary;
221         mPrimary = primary;
222 
223         if (primaryChanged && shouldShowNoteSentToast(primary)) {
224             ui.showNoteSentToast();
225         }
226 
227         // Refresh primary call information if either:
228         // 1. Primary call changed.
229         // 2. The call's ability to manage conference has changed.
230         if (mPrimary != null && (primaryChanged ||
231                 ui.isManageConferenceVisible() != shouldShowManageConference()) ||
232                 ui.isCallSubjectVisible() != shouldShowCallSubject) {
233             // primary call has changed
234             if (previousPrimary != null) {
235                 CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this);
236             }
237             CallList.getInstance().addCallUpdateListener(mPrimary.getId(), this);
238 
239             mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mPrimary,
240                     mPrimary.getState() == Call.State.INCOMING);
241             updatePrimaryDisplayInfo();
242             maybeStartSearch(mPrimary, true);
243             mPrimary.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
244         }
245 
246         if (previousPrimary != null && mPrimary == null) {
247             CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this);
248         }
249 
250         if (mSecondary == null) {
251             // Secondary call may have ended.  Update the ui.
252             mSecondaryContactInfo = null;
253             updateSecondaryDisplayInfo();
254         } else if (secondaryChanged) {
255             // secondary call has changed
256             mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mSecondary,
257                     mSecondary.getState() == Call.State.INCOMING);
258             updateSecondaryDisplayInfo();
259             maybeStartSearch(mSecondary, false);
260             mSecondary.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
261         }
262 
263         // Start/stop timers.
264         if (isPrimaryCallActive()) {
265             Log.d(this, "Starting the calltime timer");
266             mCallTimer.start(CALL_TIME_UPDATE_INTERVAL_MS);
267         } else {
268             Log.d(this, "Canceling the calltime timer");
269             mCallTimer.cancel();
270             ui.setPrimaryCallElapsedTime(false, 0);
271         }
272 
273         // Set the call state
274         int callState = Call.State.IDLE;
275         if (mPrimary != null) {
276             callState = mPrimary.getState();
277             updatePrimaryCallState();
278         } else {
279             getUi().setCallState(
280                     callState,
281                     VideoProfile.STATE_AUDIO_ONLY,
282                     Call.SessionModificationState.NO_REQUEST,
283                     new DisconnectCause(DisconnectCause.UNKNOWN),
284                     null,
285                     null,
286                     null,
287                     false /* isWifi */,
288                     false /* isConference */);
289             getUi().showHdAudioIndicator(false);
290         }
291 
292         maybeShowManageConferenceCallButton();
293 
294         // Hide the end call button instantly if we're receiving an incoming call.
295         getUi().setEndCallButtonEnabled(shouldShowEndCallButton(mPrimary, callState),
296                 callState != Call.State.INCOMING /* animate */);
297 
298         maybeSendAccessibilityEvent(oldState, newState);
299     }
300 
301     @Override
onDetailsChanged(Call call, Details details)302     public void onDetailsChanged(Call call, Details details) {
303         updatePrimaryCallState();
304 
305         if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE) !=
306                 Details.can(details.getCallCapabilities(), Details.CAPABILITY_MANAGE_CONFERENCE)) {
307             maybeShowManageConferenceCallButton();
308         }
309     }
310 
311     @Override
onCallChanged(Call call)312     public void onCallChanged(Call call) {
313         // No-op; specific call updates handled elsewhere.
314     }
315 
316     /**
317      * Handles a change to the session modification state for a call.  Triggers showing the progress
318      * spinner, as well as updating the call state label.
319      *
320      * @param sessionModificationState The new session modification state.
321      */
322     @Override
onSessionModificationStateChange(int sessionModificationState)323     public void onSessionModificationStateChange(int sessionModificationState) {
324         Log.d(this, "onSessionModificationStateChange : sessionModificationState = " +
325                 sessionModificationState);
326 
327         if (mPrimary == null) {
328             return;
329         }
330         maybeShowProgressSpinner(mPrimary.getState(), sessionModificationState);
331         getUi().setEndCallButtonEnabled(sessionModificationState !=
332                         Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
333                 true /* shouldAnimate */);
334         updatePrimaryCallState();
335     }
336 
337     /**
338      * Handles a change to the last forwarding number by refreshing the primary call info.
339      */
340     @Override
onLastForwardedNumberChange()341     public void onLastForwardedNumberChange() {
342         Log.v(this, "onLastForwardedNumberChange");
343 
344         if (mPrimary == null) {
345             return;
346         }
347         updatePrimaryDisplayInfo();
348     }
349 
350     /**
351      * Handles a change to the child number by refreshing the primary call info.
352      */
353     @Override
onChildNumberChange()354     public void onChildNumberChange() {
355         Log.v(this, "onChildNumberChange");
356 
357         if (mPrimary == null) {
358             return;
359         }
360         updatePrimaryDisplayInfo();
361     }
362 
getSubscriptionNumber()363     private String getSubscriptionNumber() {
364         // If it's an emergency call, and they're not populating the callback number,
365         // then try to fall back to the phone sub info (to hopefully get the SIM's
366         // number directly from the telephony layer).
367         PhoneAccountHandle accountHandle = mPrimary.getAccountHandle();
368         if (accountHandle != null) {
369             TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager();
370             PhoneAccount account = mgr.getPhoneAccount(accountHandle);
371             if (account != null) {
372                 return getNumberFromHandle(account.getSubscriptionAddress());
373             }
374         }
375         return null;
376     }
377 
updatePrimaryCallState()378     private void updatePrimaryCallState() {
379         if (getUi() != null && mPrimary != null) {
380             getUi().setCallState(
381                     mPrimary.getState(),
382                     mPrimary.getVideoState(),
383                     mPrimary.getSessionModificationState(),
384                     mPrimary.getDisconnectCause(),
385                     getConnectionLabel(),
386                     getCallStateIcon(),
387                     getGatewayNumber(),
388                     mPrimary.hasProperty(Details.PROPERTY_WIFI),
389                     mPrimary.isConferenceCall());
390 
391             maybeShowHdAudioIcon();
392             setCallbackNumber();
393         }
394     }
395 
396     /**
397      * Show the HD icon if the call is active and has {@link Details#PROPERTY_HIGH_DEF_AUDIO},
398      * except if the call has a last forwarded number (we will show that icon instead).
399      */
maybeShowHdAudioIcon()400     private void maybeShowHdAudioIcon() {
401         boolean showHdAudioIndicator =
402                 isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO) &&
403                 TextUtils.isEmpty(mPrimary.getLastForwardedNumber());
404         getUi().showHdAudioIndicator(showHdAudioIndicator);
405     }
406 
407     /**
408      * Only show the conference call button if we can manage the conference.
409      */
maybeShowManageConferenceCallButton()410     private void maybeShowManageConferenceCallButton() {
411         getUi().showManageConferenceCallButton(shouldShowManageConference());
412     }
413 
414     /**
415      * Determines if a pending session modification exists for the current call.  If so, the
416      * progress spinner is shown, and the call state is updated.
417      *
418      * @param callState The call state.
419      * @param sessionModificationState The session modification state.
420      */
maybeShowProgressSpinner(int callState, int sessionModificationState)421     private void maybeShowProgressSpinner(int callState, int sessionModificationState) {
422         final boolean show = sessionModificationState ==
423                 Call.SessionModificationState.WAITING_FOR_RESPONSE
424                 && callState == Call.State.ACTIVE;
425         if (show != mSpinnerShowing) {
426             getUi().setProgressSpinnerVisible(show);
427             mSpinnerShowing = show;
428         }
429     }
430 
431     /**
432      * Determines if the manage conference button should be visible, based on the current primary
433      * call.
434      *
435      * @return {@code True} if the manage conference button should be visible.
436      */
shouldShowManageConference()437     private boolean shouldShowManageConference() {
438         if (mPrimary == null) {
439             return false;
440         }
441 
442         return mPrimary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE)
443                 && !mPrimary.isVideoCall(mContext);
444     }
445 
setCallbackNumber()446     private void setCallbackNumber() {
447         String callbackNumber = null;
448 
449         // Show the emergency callback number if either:
450         // 1. This is an emergency call.
451         // 2. The phone is in Emergency Callback Mode, which means we should show the callback
452         //    number.
453         boolean showCallbackNumber = mPrimary.hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE);
454 
455         if (mPrimary.isEmergencyCall() || showCallbackNumber) {
456             callbackNumber = getSubscriptionNumber();
457         } else {
458             StatusHints statusHints = mPrimary.getTelecommCall().getDetails().getStatusHints();
459             if (statusHints != null) {
460                 Bundle extras = statusHints.getExtras();
461                 if (extras != null) {
462                     callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER);
463                 }
464             }
465         }
466 
467         TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager();
468         String simNumber = mgr.getLine1Number(mPrimary.getAccountHandle());
469         if (!showCallbackNumber && PhoneNumberUtils.compare(callbackNumber, simNumber)) {
470             Log.d(this, "Numbers are the same (and callback number is not being forced to show);" +
471                     " not showing the callback number");
472             callbackNumber = null;
473         }
474 
475         getUi().setCallbackNumber(callbackNumber, mPrimary.isEmergencyCall() || showCallbackNumber);
476     }
477 
updateCallTime()478     public void updateCallTime() {
479         final CallCardUi ui = getUi();
480 
481         if (ui == null) {
482             mCallTimer.cancel();
483         } else if (!isPrimaryCallActive()) {
484             ui.setPrimaryCallElapsedTime(false, 0);
485             mCallTimer.cancel();
486         } else {
487             final long callStart = mPrimary.getConnectTimeMillis();
488             final long duration = System.currentTimeMillis() - callStart;
489             ui.setPrimaryCallElapsedTime(true, duration);
490         }
491     }
492 
onCallStateButtonTouched()493     public void onCallStateButtonTouched() {
494         Intent broadcastIntent = ObjectFactory.getCallStateButtonBroadcastIntent(mContext);
495         if (broadcastIntent != null) {
496             Log.d(this, "Sending call state button broadcast: ", broadcastIntent);
497             mContext.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE);
498         }
499     }
500 
501     /**
502      * Handles click on the contact photo by toggling fullscreen mode if the current call is a video
503      * call.
504      */
onContactPhotoClick()505     public void onContactPhotoClick() {
506         if (mPrimary != null && mPrimary.isVideoCall(mContext)) {
507             InCallPresenter.getInstance().toggleFullscreenMode();
508         }
509     }
510 
maybeStartSearch(Call call, boolean isPrimary)511     private void maybeStartSearch(Call call, boolean isPrimary) {
512         // no need to start search for conference calls which show generic info.
513         if (call != null && !call.isConferenceCall()) {
514             startContactInfoSearch(call, isPrimary, call.getState() == Call.State.INCOMING);
515         }
516     }
517 
518     /**
519      * Starts a query for more contact data for the save primary and secondary calls.
520      */
startContactInfoSearch(final Call call, final boolean isPrimary, boolean isIncoming)521     private void startContactInfoSearch(final Call call, final boolean isPrimary,
522             boolean isIncoming) {
523         final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
524 
525         cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary));
526     }
527 
onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary)528     private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) {
529         final boolean entryMatchesExistingCall =
530                 (isPrimary && mPrimary != null && TextUtils.equals(callId,  mPrimary.getId())) ||
531                 (!isPrimary && mSecondary != null && TextUtils.equals(callId, mSecondary.getId()));
532         if (entryMatchesExistingCall) {
533             updateContactEntry(entry, isPrimary);
534         } else {
535             Log.w(this, "Dropping stale contact lookup info for " + callId);
536         }
537 
538         if (entry.name != null) {
539             Log.d(TAG, "Contact found: " + entry);
540         }
541         if (entry.contactUri != null) {
542             CallerInfoUtils.sendViewNotification(mContext, entry.contactUri);
543         }
544     }
545 
onImageLoadComplete(String callId, ContactCacheEntry entry)546     private void onImageLoadComplete(String callId, ContactCacheEntry entry) {
547         if (getUi() == null) {
548             return;
549         }
550 
551         if (entry.photo != null) {
552             if (mPrimary != null && callId.equals(mPrimary.getId())) {
553                 getUi().setPrimaryImage(entry.photo);
554             }
555         }
556     }
557 
updateContactEntry(ContactCacheEntry entry, boolean isPrimary)558     private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) {
559         if (isPrimary) {
560             mPrimaryContactInfo = entry;
561             updatePrimaryDisplayInfo();
562         } else {
563             mSecondaryContactInfo = entry;
564             updateSecondaryDisplayInfo();
565         }
566     }
567 
568     /**
569      * Get the highest priority call to display.
570      * Goes through the calls and chooses which to return based on priority of which type of call
571      * to display to the user. Callers can use the "ignore" feature to get the second best call
572      * by passing a previously found primary call as ignore.
573      *
574      * @param ignore A call to ignore if found.
575      */
getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected)576     private Call getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected) {
577 
578         // Active calls come second.  An active call always gets precedent.
579         Call retval = callList.getActiveCall();
580         if (retval != null && retval != ignore) {
581             return retval;
582         }
583 
584         // Disconnected calls get primary position if there are no active calls
585         // to let user know quickly what call has disconnected. Disconnected
586         // calls are very short lived.
587         if (!skipDisconnected) {
588             retval = callList.getDisconnectingCall();
589             if (retval != null && retval != ignore) {
590                 return retval;
591             }
592             retval = callList.getDisconnectedCall();
593             if (retval != null && retval != ignore) {
594                 return retval;
595             }
596         }
597 
598         // Then we go to background call (calls on hold)
599         retval = callList.getBackgroundCall();
600         if (retval != null && retval != ignore) {
601             return retval;
602         }
603 
604         // Lastly, we go to a second background call.
605         retval = callList.getSecondBackgroundCall();
606 
607         return retval;
608     }
609 
updatePrimaryDisplayInfo()610     private void updatePrimaryDisplayInfo() {
611         final CallCardUi ui = getUi();
612         if (ui == null) {
613             // TODO: May also occur if search result comes back after ui is destroyed. Look into
614             // removing that case completely.
615             Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!");
616             return;
617         }
618 
619         if (mPrimary == null) {
620             // Clear the primary display info.
621             ui.setPrimary(null, null, false, null, null, false);
622             return;
623         }
624 
625         if (mPrimary.isConferenceCall()) {
626             Log.d(TAG, "Update primary display info for conference call.");
627 
628             ui.setPrimary(
629                     null /* number */,
630                     getConferenceString(mPrimary),
631                     false /* nameIsNumber */,
632                     null /* label */,
633                     getConferencePhoto(mPrimary),
634                     false /* isSipCall */);
635         } else if (mPrimaryContactInfo != null) {
636             Log.d(TAG, "Update primary display info for " + mPrimaryContactInfo);
637 
638             String name = getNameForCall(mPrimaryContactInfo);
639             String number;
640 
641             boolean isChildNumberShown = !TextUtils.isEmpty(mPrimary.getChildNumber());
642             boolean isForwardedNumberShown = !TextUtils.isEmpty(mPrimary.getLastForwardedNumber());
643             boolean isCallSubjectShown = shouldShowCallSubject(mPrimary);
644 
645             if (isCallSubjectShown) {
646                 ui.setCallSubject(mPrimary.getCallSubject());
647             } else {
648                 ui.setCallSubject(null);
649             }
650 
651             if (isCallSubjectShown) {
652                 number = null;
653             } else if (isChildNumberShown) {
654                 number = mContext.getString(R.string.child_number, mPrimary.getChildNumber());
655             } else if (isForwardedNumberShown) {
656                 // Use last forwarded number instead of second line, if present.
657                 number = mPrimary.getLastForwardedNumber();
658             } else {
659                 number = getNumberForCall(mPrimaryContactInfo);
660             }
661 
662             ui.showForwardIndicator(isForwardedNumberShown);
663             maybeShowHdAudioIcon();
664 
665             boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
666             ui.setPrimary(
667                     number,
668                     name,
669                     nameIsNumber,
670                     isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label,
671                     mPrimaryContactInfo.photo,
672                     mPrimaryContactInfo.isSipCall);
673         } else {
674             // Clear the primary display info.
675             ui.setPrimary(null, null, false, null, null, false);
676         }
677 
678         if (mEmergencyCallListener != null) {
679             boolean isEmergencyCall = mPrimary.isEmergencyCall();
680             mEmergencyCallListener.onCallUpdated((BaseFragment) ui, isEmergencyCall);
681         }
682     }
683 
updateSecondaryDisplayInfo()684     private void updateSecondaryDisplayInfo() {
685         final CallCardUi ui = getUi();
686         if (ui == null) {
687             return;
688         }
689 
690         if (mSecondary == null) {
691             // Clear the secondary display info.
692             ui.setSecondary(false, null, false, null, null, false /* isConference */,
693                     false /* isVideoCall */);
694             return;
695         }
696 
697         if (mSecondary.isConferenceCall()) {
698             ui.setSecondary(
699                     true /* show */,
700                     getConferenceString(mSecondary),
701                     false /* nameIsNumber */,
702                     null /* label */,
703                     getCallProviderLabel(mSecondary),
704                     true /* isConference */,
705                     mSecondary.isVideoCall(mContext));
706         } else if (mSecondaryContactInfo != null) {
707             Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo);
708             String name = getNameForCall(mSecondaryContactInfo);
709             boolean nameIsNumber = name != null && name.equals(mSecondaryContactInfo.number);
710             ui.setSecondary(
711                     true /* show */,
712                     name,
713                     nameIsNumber,
714                     mSecondaryContactInfo.label,
715                     getCallProviderLabel(mSecondary),
716                     false /* isConference */,
717                     mSecondary.isVideoCall(mContext));
718         } else {
719             // Clear the secondary display info.
720             ui.setSecondary(false, null, false, null, null, false /* isConference */,
721                     false /* isVideoCall */);
722         }
723     }
724 
725 
726     /**
727      * Gets the phone account to display for a call.
728      */
getAccountForCall(Call call)729     private PhoneAccount getAccountForCall(Call call) {
730         PhoneAccountHandle accountHandle = call.getAccountHandle();
731         if (accountHandle == null) {
732             return null;
733         }
734         return InCallPresenter.getInstance().getTelecomManager().getPhoneAccount(accountHandle);
735     }
736 
737     /**
738      * Returns the gateway number for any existing outgoing call.
739      */
getGatewayNumber()740     private String getGatewayNumber() {
741         if (hasOutgoingGatewayCall()) {
742             return getNumberFromHandle(mPrimary.getGatewayInfo().getGatewayAddress());
743         }
744         return null;
745     }
746 
747     /**
748      * Return the string label to represent the call provider
749      */
getCallProviderLabel(Call call)750     private String getCallProviderLabel(Call call) {
751         PhoneAccount account = getAccountForCall(call);
752         TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager();
753         if (account != null && !TextUtils.isEmpty(account.getLabel())
754                 && mgr.getCallCapablePhoneAccounts().size() > 1) {
755             return account.getLabel().toString();
756         }
757         return null;
758     }
759 
760     /**
761      * Returns the label (line of text above the number/name) for any given call.
762      * For example, "calling via [Account/Google Voice]" for outgoing calls.
763      */
getConnectionLabel()764     private String getConnectionLabel() {
765         StatusHints statusHints = mPrimary.getTelecommCall().getDetails().getStatusHints();
766         if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) {
767             return statusHints.getLabel().toString();
768         }
769 
770         if (hasOutgoingGatewayCall() && getUi() != null) {
771             // Return the label for the gateway app on outgoing calls.
772             final PackageManager pm = mContext.getPackageManager();
773             try {
774                 ApplicationInfo info = pm.getApplicationInfo(
775                         mPrimary.getGatewayInfo().getGatewayProviderPackageName(), 0);
776                 return pm.getApplicationLabel(info).toString();
777             } catch (PackageManager.NameNotFoundException e) {
778                 Log.e(this, "Gateway Application Not Found.", e);
779                 return null;
780             }
781         }
782         return getCallProviderLabel(mPrimary);
783     }
784 
getCallStateIcon()785     private Drawable getCallStateIcon() {
786         // Return connection icon if one exists.
787         StatusHints statusHints = mPrimary.getTelecommCall().getDetails().getStatusHints();
788         if (statusHints != null && statusHints.getIcon() != null) {
789             Drawable icon = statusHints.getIcon().loadDrawable(mContext);
790             if (icon != null) {
791                 return icon;
792             }
793         }
794 
795         return null;
796     }
797 
hasOutgoingGatewayCall()798     private boolean hasOutgoingGatewayCall() {
799         // We only display the gateway information while STATE_DIALING so return false for any other
800         // call state.
801         // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
802         // is also called after a contact search completes (call is not present yet).  Split the
803         // UI update so it can receive independent updates.
804         if (mPrimary == null) {
805             return false;
806         }
807         return Call.State.isDialing(mPrimary.getState()) && mPrimary.getGatewayInfo() != null &&
808                 !mPrimary.getGatewayInfo().isEmpty();
809     }
810 
811     /**
812      * Gets the name to display for the call.
813      */
getNameForCall(ContactCacheEntry contactInfo)814     private static String getNameForCall(ContactCacheEntry contactInfo) {
815         if (TextUtils.isEmpty(contactInfo.name)) {
816             return contactInfo.number;
817         }
818         return contactInfo.name;
819     }
820 
821     /**
822      * Gets the number to display for a call.
823      */
getNumberForCall(ContactCacheEntry contactInfo)824     private static String getNumberForCall(ContactCacheEntry contactInfo) {
825         // If the name is empty, we use the number for the name...so dont show a second
826         // number in the number field
827         if (TextUtils.isEmpty(contactInfo.name)) {
828             return contactInfo.location;
829         }
830         return contactInfo.number;
831     }
832 
secondaryInfoClicked()833     public void secondaryInfoClicked() {
834         if (mSecondary == null) {
835             Log.w(this, "Secondary info clicked but no secondary call.");
836             return;
837         }
838 
839         Log.i(this, "Swapping call to foreground: " + mSecondary);
840         TelecomAdapter.getInstance().unholdCall(mSecondary.getId());
841     }
842 
endCallClicked()843     public void endCallClicked() {
844         if (mPrimary == null) {
845             return;
846         }
847 
848         Log.i(this, "Disconnecting call: " + mPrimary);
849         final String callId = mPrimary.getId();
850         mPrimary.setState(Call.State.DISCONNECTING);
851         CallList.getInstance().onUpdate(mPrimary);
852         TelecomAdapter.getInstance().disconnectCall(callId);
853     }
854 
getNumberFromHandle(Uri handle)855     private String getNumberFromHandle(Uri handle) {
856         return handle == null ? "" : handle.getSchemeSpecificPart();
857     }
858 
859     /**
860      * Handles a change to the fullscreen mode of the in-call UI.
861      *
862      * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode.
863      */
864     @Override
onFullscreenModeChanged(boolean isFullscreenMode)865     public void onFullscreenModeChanged(boolean isFullscreenMode) {
866         final CallCardUi ui = getUi();
867         if (ui == null) {
868             return;
869         }
870         ui.setCallCardVisible(!isFullscreenMode);
871     }
872 
isPrimaryCallActive()873     private boolean isPrimaryCallActive() {
874         return mPrimary != null && mPrimary.getState() == Call.State.ACTIVE;
875     }
876 
getConferenceString(Call call)877     private String getConferenceString(Call call) {
878         boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
879         Log.v(this, "getConferenceString: " + isGenericConference);
880 
881         final int resId = isGenericConference
882                 ? R.string.card_title_in_call : R.string.card_title_conf_call;
883         return mContext.getResources().getString(resId);
884     }
885 
getConferencePhoto(Call call)886     private Drawable getConferencePhoto(Call call) {
887         boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
888         Log.v(this, "getConferencePhoto: " + isGenericConference);
889 
890         final int resId = isGenericConference
891                 ? R.drawable.img_phone : R.drawable.img_conference;
892         Drawable photo = mContext.getResources().getDrawable(resId);
893         photo.setAutoMirrored(true);
894         return photo;
895     }
896 
shouldShowEndCallButton(Call primary, int callState)897     private boolean shouldShowEndCallButton(Call primary, int callState) {
898         if (primary == null) {
899             return false;
900         }
901         if ((!Call.State.isConnectingOrConnected(callState)
902                 && callState != Call.State.DISCONNECTING) || callState == Call.State.INCOMING) {
903             return false;
904         }
905         if (mPrimary.getSessionModificationState()
906                 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
907             return false;
908         }
909         return true;
910     }
911 
maybeSendAccessibilityEvent(InCallState oldState, InCallState newState)912     private void maybeSendAccessibilityEvent(InCallState oldState, InCallState newState) {
913         if (mContext == null) {
914             return;
915         }
916         final AccessibilityManager am = (AccessibilityManager) mContext.getSystemService(
917                 Context.ACCESSIBILITY_SERVICE);
918         if (!am.isEnabled()) {
919             return;
920         }
921         if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING)
922                 || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING)) {
923             if (getUi() != null) {
924                 getUi().sendAccessibilityAnnouncement();
925             }
926         }
927     }
928 
929     /**
930      * Determines whether the call subject should be visible on the UI.  For the call subject to be
931      * visible, the call has to be in an incoming or waiting state, and the subject must not be
932      * empty.
933      *
934      * @param call The call.
935      * @return {@code true} if the subject should be shown, {@code false} otherwise.
936      */
shouldShowCallSubject(Call call)937     private boolean shouldShowCallSubject(Call call) {
938         if (call == null) {
939             return false;
940         }
941 
942         boolean isIncomingOrWaiting = mPrimary.getState() == Call.State.INCOMING ||
943                 mPrimary.getState() == Call.State.CALL_WAITING;
944         return isIncomingOrWaiting && !TextUtils.isEmpty(call.getCallSubject()) &&
945                 call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED &&
946                 call.isCallSubjectSupported();
947     }
948 
949     /**
950      * Determines whether the "note sent" toast should be shown.  It should be shown for a new
951      * outgoing call with a subject.
952      *
953      * @param call The call
954      * @return {@code true} if the toast should be shown, {@code false} otherwise.
955      */
shouldShowNoteSentToast(Call call)956     private boolean shouldShowNoteSentToast(Call call) {
957         return call != null && !TextUtils
958                 .isEmpty(call.getTelecommCall().getDetails().getIntentExtras().getString(
959                         TelecomManager.EXTRA_CALL_SUBJECT)) &&
960                 (call.getState() == Call.State.DIALING || call.getState() == Call.State.CONNECTING);
961     }
962 
963     public interface CallCardUi extends Ui {
setVisible(boolean on)964         void setVisible(boolean on);
setCallCardVisible(boolean visible)965         void setCallCardVisible(boolean visible);
setPrimary(String number, String name, boolean nameIsNumber, String label, Drawable photo, boolean isSipCall)966         void setPrimary(String number, String name, boolean nameIsNumber, String label,
967                 Drawable photo, boolean isSipCall);
setSecondary(boolean show, String name, boolean nameIsNumber, String label, String providerLabel, boolean isConference, boolean isVideoCall)968         void setSecondary(boolean show, String name, boolean nameIsNumber, String label,
969                 String providerLabel, boolean isConference, boolean isVideoCall);
setCallState(int state, int videoState, int sessionModificationState, DisconnectCause disconnectCause, String connectionLabel, Drawable connectionIcon, String gatewayNumber, boolean isWifi, boolean isConference)970         void setCallState(int state, int videoState, int sessionModificationState,
971                 DisconnectCause disconnectCause, String connectionLabel,
972                 Drawable connectionIcon, String gatewayNumber, boolean isWifi,
973                 boolean isConference);
setPrimaryCallElapsedTime(boolean show, long duration)974         void setPrimaryCallElapsedTime(boolean show, long duration);
setPrimaryName(String name, boolean nameIsNumber)975         void setPrimaryName(String name, boolean nameIsNumber);
setPrimaryImage(Drawable image)976         void setPrimaryImage(Drawable image);
setPrimaryPhoneNumber(String phoneNumber)977         void setPrimaryPhoneNumber(String phoneNumber);
setPrimaryLabel(String label)978         void setPrimaryLabel(String label);
setEndCallButtonEnabled(boolean enabled, boolean animate)979         void setEndCallButtonEnabled(boolean enabled, boolean animate);
setCallbackNumber(String number, boolean isEmergencyCalls)980         void setCallbackNumber(String number, boolean isEmergencyCalls);
setCallSubject(String callSubject)981         void setCallSubject(String callSubject);
setProgressSpinnerVisible(boolean visible)982         void setProgressSpinnerVisible(boolean visible);
showHdAudioIndicator(boolean visible)983         void showHdAudioIndicator(boolean visible);
showForwardIndicator(boolean visible)984         void showForwardIndicator(boolean visible);
showManageConferenceCallButton(boolean visible)985         void showManageConferenceCallButton(boolean visible);
isManageConferenceVisible()986         boolean isManageConferenceVisible();
isCallSubjectVisible()987         boolean isCallSubjectVisible();
animateForNewOutgoingCall()988         void animateForNewOutgoingCall();
sendAccessibilityAnnouncement()989         void sendAccessibilityAnnouncement();
showNoteSentToast()990         void showNoteSentToast();
991     }
992 }
993