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