/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.incallui;
import com.google.common.base.Preconditions;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.telecom.Call.Details;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.widget.ListAdapter;
import com.android.contacts.common.ContactsUtils;
import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.testing.NeededForTesting;
import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.dialer.R;
import com.android.incallui.Call.State;
import com.android.incallui.ContactInfoCache.ContactCacheEntry;
import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
import com.android.incallui.InCallPresenter.InCallDetailsListener;
import com.android.incallui.InCallPresenter.InCallEventListener;
import com.android.incallui.InCallPresenter.InCallState;
import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.incalluibind.ObjectFactory;
import java.lang.ref.WeakReference;
import static com.android.contacts.common.compat.CallSdkCompat.Details.PROPERTY_ENTERPRISE_CALL;
/**
* Presenter for the Call Card Fragment.
*
* This class listens for changes to InCallState and passes it along to the fragment.
*/
public class CallCardPresenter extends Presenter
implements InCallStateListener, IncomingCallListener, InCallDetailsListener,
InCallEventListener, CallList.CallUpdateListener, DistanceHelper.Listener {
public interface EmergencyCallListener {
public void onCallUpdated(BaseFragment fragment, boolean isEmergency);
}
private static final String TAG = CallCardPresenter.class.getSimpleName();
private static final long CALL_TIME_UPDATE_INTERVAL_MS = 1000;
private final EmergencyCallListener mEmergencyCallListener =
ObjectFactory.newEmergencyCallListener();
private DistanceHelper mDistanceHelper;
private Call mPrimary;
private Call mSecondary;
private ContactCacheEntry mPrimaryContactInfo;
private ContactCacheEntry mSecondaryContactInfo;
private CallTimer mCallTimer;
private Context mContext;
@Nullable private ContactsPreferences mContactsPreferences;
private boolean mSpinnerShowing = false;
private boolean mHasShownToast = false;
private InCallContactInteractions mInCallContactInteractions;
private boolean mIsFullscreen = false;
public static class ContactLookupCallback implements ContactInfoCacheCallback {
private final WeakReference mCallCardPresenter;
private final boolean mIsPrimary;
public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) {
mCallCardPresenter = new WeakReference(callCardPresenter);
mIsPrimary = isPrimary;
}
@Override
public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
CallCardPresenter presenter = mCallCardPresenter.get();
if (presenter != null) {
presenter.onContactInfoComplete(callId, entry, mIsPrimary);
}
}
@Override
public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
CallCardPresenter presenter = mCallCardPresenter.get();
if (presenter != null) {
presenter.onImageLoadComplete(callId, entry);
}
}
@Override
public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {
CallCardPresenter presenter = mCallCardPresenter.get();
if (presenter != null) {
presenter.onContactInteractionsInfoComplete(callId, entry);
}
}
}
public CallCardPresenter() {
// create the call timer
mCallTimer = new CallTimer(new Runnable() {
@Override
public void run() {
updateCallTime();
}
});
}
public void init(Context context, Call call) {
mContext = Preconditions.checkNotNull(context);
mDistanceHelper = ObjectFactory.newDistanceHelper(mContext, this);
mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
// Call may be null if disconnect happened already.
if (call != null) {
mPrimary = call;
if (shouldShowNoteSentToast(mPrimary)) {
final CallCardUi ui = getUi();
if (ui != null) {
ui.showNoteSentToast();
}
}
CallList.getInstance().addCallUpdateListener(call.getId(), this);
// start processing lookups right away.
if (!call.isConferenceCall()) {
startContactInfoSearch(call, true, call.getState() == Call.State.INCOMING);
} else {
updateContactEntry(null, true);
}
}
onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance());
}
@Override
public void onUiReady(CallCardUi ui) {
super.onUiReady(ui);
if (mContactsPreferences != null) {
mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
}
// Contact search may have completed before ui is ready.
if (mPrimaryContactInfo != null) {
updatePrimaryDisplayInfo();
}
// Register for call state changes last
InCallPresenter.getInstance().addListener(this);
InCallPresenter.getInstance().addIncomingCallListener(this);
InCallPresenter.getInstance().addDetailsListener(this);
InCallPresenter.getInstance().addInCallEventListener(this);
}
@Override
public void onUiUnready(CallCardUi ui) {
super.onUiUnready(ui);
// stop getting call state changes
InCallPresenter.getInstance().removeListener(this);
InCallPresenter.getInstance().removeIncomingCallListener(this);
InCallPresenter.getInstance().removeDetailsListener(this);
InCallPresenter.getInstance().removeInCallEventListener(this);
if (mPrimary != null) {
CallList.getInstance().removeCallUpdateListener(mPrimary.getId(), this);
}
if (mDistanceHelper != null) {
mDistanceHelper.cleanUp();
}
mPrimary = null;
mPrimaryContactInfo = null;
mSecondaryContactInfo = null;
}
@Override
public void onIncomingCall(InCallState oldState, InCallState newState, Call call) {
// same logic should happen as with onStateChange()
onStateChange(oldState, newState, CallList.getInstance());
}
@Override
public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
Log.d(this, "onStateChange() " + newState);
final CallCardUi ui = getUi();
if (ui == null) {
return;
}
Call primary = null;
Call secondary = null;
if (newState == InCallState.INCOMING) {
primary = callList.getIncomingCall();
} else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) {
primary = callList.getOutgoingCall();
if (primary == null) {
primary = callList.getPendingOutgoingCall();
}
// getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
// highest priority call to display as the secondary call.
secondary = getCallToDisplay(callList, null, true);
} else if (newState == InCallState.INCALL) {
primary = getCallToDisplay(callList, null, false);
secondary = getCallToDisplay(callList, primary, true);
}
if (mInCallContactInteractions != null &&
(oldState == InCallState.INCOMING || newState == InCallState.INCOMING)) {
ui.showContactContext(newState != InCallState.INCOMING);
}
Log.d(this, "Primary call: " + primary);
Log.d(this, "Secondary call: " + secondary);
final boolean primaryChanged = !(Call.areSame(mPrimary, primary) &&
Call.areSameNumber(mPrimary, primary));
final boolean secondaryChanged = !(Call.areSame(mSecondary, secondary) &&
Call.areSameNumber(mSecondary, secondary));
mSecondary = secondary;
Call previousPrimary = mPrimary;
mPrimary = primary;
if (primaryChanged && shouldShowNoteSentToast(primary)) {
ui.showNoteSentToast();
}
// Refresh primary call information if either:
// 1. Primary call changed.
// 2. The call's ability to manage conference has changed.
// 3. The call subject should be shown or hidden.
if (shouldRefreshPrimaryInfo(primaryChanged, ui, shouldShowCallSubject(mPrimary))) {
// primary call has changed
if (previousPrimary != null) {
//clear progess spinner (if any) related to previous primary call
maybeShowProgressSpinner(previousPrimary.getState(),
Call.SessionModificationState.NO_REQUEST);
CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this);
}
CallList.getInstance().addCallUpdateListener(mPrimary.getId(), this);
mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mPrimary,
mPrimary.getState() == Call.State.INCOMING);
updatePrimaryDisplayInfo();
maybeStartSearch(mPrimary, true);
maybeClearSessionModificationState(mPrimary);
}
if (previousPrimary != null && mPrimary == null) {
//clear progess spinner (if any) related to previous primary call
maybeShowProgressSpinner(previousPrimary.getState(),
Call.SessionModificationState.NO_REQUEST);
CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this);
}
if (mSecondary == null) {
// Secondary call may have ended. Update the ui.
mSecondaryContactInfo = null;
updateSecondaryDisplayInfo();
} else if (secondaryChanged) {
// secondary call has changed
mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mSecondary,
mSecondary.getState() == Call.State.INCOMING);
updateSecondaryDisplayInfo();
maybeStartSearch(mSecondary, false);
maybeClearSessionModificationState(mSecondary);
}
// Start/stop timers.
if (isPrimaryCallActive()) {
Log.d(this, "Starting the calltime timer");
mCallTimer.start(CALL_TIME_UPDATE_INTERVAL_MS);
} else {
Log.d(this, "Canceling the calltime timer");
mCallTimer.cancel();
ui.setPrimaryCallElapsedTime(false, 0);
}
// Set the call state
int callState = Call.State.IDLE;
if (mPrimary != null) {
callState = mPrimary.getState();
updatePrimaryCallState();
} else {
getUi().setCallState(
callState,
VideoProfile.STATE_AUDIO_ONLY,
Call.SessionModificationState.NO_REQUEST,
new DisconnectCause(DisconnectCause.UNKNOWN),
null,
null,
null,
false /* isWifi */,
false /* isConference */,
false /* isWorkCall */);
getUi().showHdAudioIndicator(false);
}
maybeShowManageConferenceCallButton();
// Hide the end call button instantly if we're receiving an incoming call.
getUi().setEndCallButtonEnabled(shouldShowEndCallButton(mPrimary, callState),
callState != Call.State.INCOMING /* animate */);
maybeSendAccessibilityEvent(oldState, newState, primaryChanged);
}
@Override
public void onDetailsChanged(Call call, Details details) {
updatePrimaryCallState();
if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE) !=
details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) {
maybeShowManageConferenceCallButton();
}
}
@Override
public void onCallChanged(Call call) {
// No-op; specific call updates handled elsewhere.
}
/**
* Handles a change to the session modification state for a call. Triggers showing the progress
* spinner, as well as updating the call state label.
*
* @param sessionModificationState The new session modification state.
*/
@Override
public void onSessionModificationStateChange(int sessionModificationState) {
Log.d(this, "onSessionModificationStateChange : sessionModificationState = " +
sessionModificationState);
if (mPrimary == null) {
return;
}
maybeShowProgressSpinner(mPrimary.getState(), sessionModificationState);
getUi().setEndCallButtonEnabled(sessionModificationState !=
Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
true /* shouldAnimate */);
updatePrimaryCallState();
}
/**
* Handles a change to the last forwarding number by refreshing the primary call info.
*/
@Override
public void onLastForwardedNumberChange() {
Log.v(this, "onLastForwardedNumberChange");
if (mPrimary == null) {
return;
}
updatePrimaryDisplayInfo();
}
/**
* Handles a change to the child number by refreshing the primary call info.
*/
@Override
public void onChildNumberChange() {
Log.v(this, "onChildNumberChange");
if (mPrimary == null) {
return;
}
updatePrimaryDisplayInfo();
}
private boolean shouldRefreshPrimaryInfo(boolean primaryChanged, CallCardUi ui,
boolean shouldShowCallSubject) {
if (mPrimary == null) {
return false;
}
return primaryChanged ||
ui.isManageConferenceVisible() != shouldShowManageConference() ||
ui.isCallSubjectVisible() != shouldShowCallSubject;
}
private String getSubscriptionNumber() {
// If it's an emergency call, and they're not populating the callback number,
// then try to fall back to the phone sub info (to hopefully get the SIM's
// number directly from the telephony layer).
PhoneAccountHandle accountHandle = mPrimary.getAccountHandle();
if (accountHandle != null) {
TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager();
PhoneAccount account = TelecomManagerCompat.getPhoneAccount(mgr, accountHandle);
if (account != null) {
return getNumberFromHandle(account.getSubscriptionAddress());
}
}
return null;
}
private void updatePrimaryCallState() {
if (getUi() != null && mPrimary != null) {
boolean isWorkCall = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL)
|| (mPrimaryContactInfo == null ? false
: mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
getUi().setCallState(
mPrimary.getState(),
mPrimary.getVideoState(),
mPrimary.getSessionModificationState(),
mPrimary.getDisconnectCause(),
getConnectionLabel(),
getCallStateIcon(),
getGatewayNumber(),
mPrimary.hasProperty(Details.PROPERTY_WIFI),
mPrimary.isConferenceCall(),
isWorkCall);
maybeShowHdAudioIcon();
setCallbackNumber();
}
}
/**
* Show the HD icon if the call is active and has {@link Details#PROPERTY_HIGH_DEF_AUDIO},
* except if the call has a last forwarded number (we will show that icon instead).
*/
private void maybeShowHdAudioIcon() {
boolean showHdAudioIndicator =
isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO) &&
TextUtils.isEmpty(mPrimary.getLastForwardedNumber());
getUi().showHdAudioIndicator(showHdAudioIndicator);
}
/**
* Only show the conference call button if we can manage the conference.
*/
private void maybeShowManageConferenceCallButton() {
getUi().showManageConferenceCallButton(shouldShowManageConference());
}
/**
* Determines if a pending session modification exists for the current call. If so, the
* progress spinner is shown, and the call state is updated.
*
* @param callState The call state.
* @param sessionModificationState The session modification state.
*/
private void maybeShowProgressSpinner(int callState, int sessionModificationState) {
final boolean show = sessionModificationState ==
Call.SessionModificationState.WAITING_FOR_RESPONSE
&& callState == Call.State.ACTIVE;
if (show != mSpinnerShowing) {
getUi().setProgressSpinnerVisible(show);
mSpinnerShowing = show;
}
}
/**
* Determines if the manage conference button should be visible, based on the current primary
* call.
*
* @return {@code True} if the manage conference button should be visible.
*/
private boolean shouldShowManageConference() {
if (mPrimary == null) {
return false;
}
return mPrimary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE)
&& !mIsFullscreen;
}
private void setCallbackNumber() {
String callbackNumber = null;
// Show the emergency callback number if either:
// 1. This is an emergency call.
// 2. The phone is in Emergency Callback Mode, which means we should show the callback
// number.
boolean showCallbackNumber = mPrimary.hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE);
if (mPrimary.isEmergencyCall() || showCallbackNumber) {
callbackNumber = getSubscriptionNumber();
} else {
StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints();
if (statusHints != null) {
Bundle extras = statusHints.getExtras();
if (extras != null) {
callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER);
}
}
}
final String simNumber = TelecomManagerCompat.getLine1Number(
InCallPresenter.getInstance().getTelecomManager(),
InCallPresenter.getInstance().getTelephonyManager(),
mPrimary.getAccountHandle());
if (!showCallbackNumber && PhoneNumberUtils.compare(callbackNumber, simNumber)) {
Log.d(this, "Numbers are the same (and callback number is not being forced to show);" +
" not showing the callback number");
callbackNumber = null;
}
getUi().setCallbackNumber(callbackNumber, mPrimary.isEmergencyCall() || showCallbackNumber);
}
public void updateCallTime() {
final CallCardUi ui = getUi();
if (ui == null) {
mCallTimer.cancel();
} else if (!isPrimaryCallActive()) {
ui.setPrimaryCallElapsedTime(false, 0);
mCallTimer.cancel();
} else {
final long callStart = mPrimary.getConnectTimeMillis();
final long duration = System.currentTimeMillis() - callStart;
ui.setPrimaryCallElapsedTime(true, duration);
}
}
public void onCallStateButtonTouched() {
Intent broadcastIntent = ObjectFactory.getCallStateButtonBroadcastIntent(mContext);
if (broadcastIntent != null) {
Log.d(this, "Sending call state button broadcast: ", broadcastIntent);
mContext.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE);
}
}
/**
* Handles click on the contact photo by toggling fullscreen mode if the current call is a video
* call.
*/
public void onContactPhotoClick() {
if (mPrimary != null && mPrimary.isVideoCall(mContext)) {
InCallPresenter.getInstance().toggleFullscreenMode();
}
}
private void maybeStartSearch(Call call, boolean isPrimary) {
// no need to start search for conference calls which show generic info.
if (call != null && !call.isConferenceCall()) {
startContactInfoSearch(call, isPrimary, call.getState() == Call.State.INCOMING);
}
}
private void maybeClearSessionModificationState(Call call) {
if (call.getSessionModificationState() !=
Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
}
}
/**
* Starts a query for more contact data for the save primary and secondary calls.
*/
private void startContactInfoSearch(final Call call, final boolean isPrimary,
boolean isIncoming) {
final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary));
}
private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) {
final boolean entryMatchesExistingCall =
(isPrimary && mPrimary != null && TextUtils.equals(callId, mPrimary.getId())) ||
(!isPrimary && mSecondary != null && TextUtils.equals(callId, mSecondary.getId()));
if (entryMatchesExistingCall) {
updateContactEntry(entry, isPrimary);
} else {
Log.w(this, "Dropping stale contact lookup info for " + callId);
}
final Call call = CallList.getInstance().getCallById(callId);
if (call != null) {
call.getLogState().contactLookupResult = entry.contactLookupResult;
}
if (entry.contactUri != null) {
CallerInfoUtils.sendViewNotification(mContext, entry.contactUri);
}
}
private void onImageLoadComplete(String callId, ContactCacheEntry entry) {
if (getUi() == null) {
return;
}
if (entry.photo != null) {
if (mPrimary != null && callId.equals(mPrimary.getId())) {
boolean showContactPhoto = !VideoCallPresenter.showIncomingVideo(
mPrimary.getVideoState(), mPrimary.getState());
getUi().setPrimaryImage(entry.photo, showContactPhoto);
}
}
}
private void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {
if (getUi() == null) {
return;
}
if (mPrimary != null && callId.equals(mPrimary.getId())) {
mPrimaryContactInfo.locationAddress = entry.locationAddress;
updateContactInteractions();
}
}
@Override
public void onLocationReady() {
// This will only update the contacts interactions data if the location returns after
// the contact information is found.
updateContactInteractions();
}
private void updateContactInteractions() {
if (mPrimary != null && mPrimaryContactInfo != null
&& (mPrimaryContactInfo.locationAddress != null
|| mPrimaryContactInfo.openingHours != null)) {
// TODO: This is hardcoded to "isBusiness" because functionality to differentiate
// between business and personal has not yet been added.
if (setInCallContactInteractionsType(true /* isBusiness */)) {
getUi().setContactContextTitle(
mInCallContactInteractions.getBusinessListHeaderView());
}
mInCallContactInteractions.setBusinessInfo(
mPrimaryContactInfo.locationAddress,
mDistanceHelper.calculateDistance(mPrimaryContactInfo.locationAddress),
mPrimaryContactInfo.openingHours);
getUi().setContactContextContent(mInCallContactInteractions.getListAdapter());
getUi().showContactContext(mPrimary.getState() != State.INCOMING);
} else {
getUi().showContactContext(false);
}
}
/**
* Update the contact interactions type so that the correct UI is shown.
*
* @param isBusiness {@code true} if the interaction is a business interaction, {@code false} if
* it is a personal contact.
*
* @return {@code true} if this is a new type of contact interaction (business or personal).
* {@code false} if it hasn't changed.
*/
private boolean setInCallContactInteractionsType(boolean isBusiness) {
if (mInCallContactInteractions == null) {
mInCallContactInteractions =
new InCallContactInteractions(mContext, isBusiness);
return true;
}
return mInCallContactInteractions.switchContactType(isBusiness);
}
private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) {
if (isPrimary) {
mPrimaryContactInfo = entry;
updatePrimaryDisplayInfo();
} else {
mSecondaryContactInfo = entry;
updateSecondaryDisplayInfo();
}
}
/**
* Get the highest priority call to display.
* Goes through the calls and chooses which to return based on priority of which type of call
* to display to the user. Callers can use the "ignore" feature to get the second best call
* by passing a previously found primary call as ignore.
*
* @param ignore A call to ignore if found.
*/
private Call getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected) {
// Active calls come second. An active call always gets precedent.
Call retval = callList.getActiveCall();
if (retval != null && retval != ignore) {
return retval;
}
// Sometimes there is intemediate state that two calls are in active even one is about
// to be on hold.
retval = callList.getSecondActiveCall();
if (retval != null && retval != ignore) {
return retval;
}
// Disconnected calls get primary position if there are no active calls
// to let user know quickly what call has disconnected. Disconnected
// calls are very short lived.
if (!skipDisconnected) {
retval = callList.getDisconnectingCall();
if (retval != null && retval != ignore) {
return retval;
}
retval = callList.getDisconnectedCall();
if (retval != null && retval != ignore) {
return retval;
}
}
// Then we go to background call (calls on hold)
retval = callList.getBackgroundCall();
if (retval != null && retval != ignore) {
return retval;
}
// Lastly, we go to a second background call.
retval = callList.getSecondBackgroundCall();
return retval;
}
private void updatePrimaryDisplayInfo() {
final CallCardUi ui = getUi();
if (ui == null) {
// TODO: May also occur if search result comes back after ui is destroyed. Look into
// removing that case completely.
Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!");
return;
}
if (mPrimary == null) {
// Clear the primary display info.
ui.setPrimary(null, null, false, null, null, false, false, false);
return;
}
// Hide the contact photo if we are in a video call and the incoming video surface is
// showing.
boolean showContactPhoto = !VideoCallPresenter
.showIncomingVideo(mPrimary.getVideoState(), mPrimary.getState());
// Call placed through a work phone account.
boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL);
if (mPrimary.isConferenceCall()) {
Log.d(TAG, "Update primary display info for conference call.");
ui.setPrimary(
null /* number */,
getConferenceString(mPrimary),
false /* nameIsNumber */,
null /* label */,
getConferencePhoto(mPrimary),
false /* isSipCall */,
showContactPhoto,
hasWorkCallProperty);
} else if (mPrimaryContactInfo != null) {
Log.d(TAG, "Update primary display info for " + mPrimaryContactInfo);
String name = getNameForCall(mPrimaryContactInfo);
String number;
boolean isChildNumberShown = !TextUtils.isEmpty(mPrimary.getChildNumber());
boolean isForwardedNumberShown = !TextUtils.isEmpty(mPrimary.getLastForwardedNumber());
boolean isCallSubjectShown = shouldShowCallSubject(mPrimary);
if (isCallSubjectShown) {
ui.setCallSubject(mPrimary.getCallSubject());
} else {
ui.setCallSubject(null);
}
if (isCallSubjectShown) {
number = null;
} else if (isChildNumberShown) {
number = mContext.getString(R.string.child_number, mPrimary.getChildNumber());
} else if (isForwardedNumberShown) {
// Use last forwarded number instead of second line, if present.
number = mPrimary.getLastForwardedNumber();
} else {
number = getNumberForCall(mPrimaryContactInfo);
}
ui.showForwardIndicator(isForwardedNumberShown);
maybeShowHdAudioIcon();
boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
// Call with caller that is a work contact.
boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
ui.setPrimary(
number,
name,
nameIsNumber,
isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label,
mPrimaryContactInfo.photo,
mPrimaryContactInfo.isSipCall,
showContactPhoto,
hasWorkCallProperty || isWorkContact);
updateContactInteractions();
} else {
// Clear the primary display info.
ui.setPrimary(null, null, false, null, null, false, false, false);
}
if (mEmergencyCallListener != null) {
boolean isEmergencyCall = mPrimary.isEmergencyCall();
mEmergencyCallListener.onCallUpdated((BaseFragment) ui, isEmergencyCall);
}
}
private void updateSecondaryDisplayInfo() {
final CallCardUi ui = getUi();
if (ui == null) {
return;
}
if (mSecondary == null) {
// Clear the secondary display info.
ui.setSecondary(false, null, false, null, null, false /* isConference */,
false /* isVideoCall */, mIsFullscreen);
return;
}
if (mSecondary.isConferenceCall()) {
ui.setSecondary(
true /* show */,
getConferenceString(mSecondary),
false /* nameIsNumber */,
null /* label */,
getCallProviderLabel(mSecondary),
true /* isConference */,
mSecondary.isVideoCall(mContext),
mIsFullscreen);
} else if (mSecondaryContactInfo != null) {
Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo);
String name = getNameForCall(mSecondaryContactInfo);
boolean nameIsNumber = name != null && name.equals(mSecondaryContactInfo.number);
ui.setSecondary(
true /* show */,
name,
nameIsNumber,
mSecondaryContactInfo.label,
getCallProviderLabel(mSecondary),
false /* isConference */,
mSecondary.isVideoCall(mContext),
mIsFullscreen);
} else {
// Clear the secondary display info.
ui.setSecondary(false, null, false, null, null, false /* isConference */,
false /* isVideoCall */, mIsFullscreen);
}
}
/**
* Gets the phone account to display for a call.
*/
private PhoneAccount getAccountForCall(Call call) {
PhoneAccountHandle accountHandle = call.getAccountHandle();
if (accountHandle == null) {
return null;
}
return TelecomManagerCompat.getPhoneAccount(
InCallPresenter.getInstance().getTelecomManager(),
accountHandle);
}
/**
* Returns the gateway number for any existing outgoing call.
*/
private String getGatewayNumber() {
if (hasOutgoingGatewayCall()) {
return getNumberFromHandle(mPrimary.getGatewayInfo().getGatewayAddress());
}
return null;
}
/**
* Return the string label to represent the call provider
*/
private String getCallProviderLabel(Call call) {
PhoneAccount account = getAccountForCall(call);
TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager();
if (account != null && !TextUtils.isEmpty(account.getLabel())
&& TelecomManagerCompat.getCallCapablePhoneAccounts(mgr).size() > 1) {
return account.getLabel().toString();
}
return null;
}
/**
* Returns the label (line of text above the number/name) for any given call.
* For example, "calling via [Account/Google Voice]" for outgoing calls.
*/
private String getConnectionLabel() {
StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints();
if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) {
return statusHints.getLabel().toString();
}
if (hasOutgoingGatewayCall() && getUi() != null) {
// Return the label for the gateway app on outgoing calls.
final PackageManager pm = mContext.getPackageManager();
try {
ApplicationInfo info = pm.getApplicationInfo(
mPrimary.getGatewayInfo().getGatewayProviderPackageName(), 0);
return pm.getApplicationLabel(info).toString();
} catch (PackageManager.NameNotFoundException e) {
Log.e(this, "Gateway Application Not Found.", e);
return null;
}
}
return getCallProviderLabel(mPrimary);
}
private Drawable getCallStateIcon() {
// Return connection icon if one exists.
StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints();
if (statusHints != null && statusHints.getIcon() != null) {
Drawable icon = statusHints.getIcon().loadDrawable(mContext);
if (icon != null) {
return icon;
}
}
return null;
}
private boolean hasOutgoingGatewayCall() {
// We only display the gateway information while STATE_DIALING so return false for any other
// call state.
// TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
// is also called after a contact search completes (call is not present yet). Split the
// UI update so it can receive independent updates.
if (mPrimary == null) {
return false;
}
return Call.State.isDialing(mPrimary.getState()) && mPrimary.getGatewayInfo() != null &&
!mPrimary.getGatewayInfo().isEmpty();
}
/**
* Gets the name to display for the call.
*/
@NeededForTesting
String getNameForCall(ContactCacheEntry contactInfo) {
String preferredName = ContactDisplayUtils.getPreferredDisplayName(
contactInfo.namePrimary,
contactInfo.nameAlternative,
mContactsPreferences);
if (TextUtils.isEmpty(preferredName)) {
return contactInfo.number;
}
return preferredName;
}
/**
* Gets the number to display for a call.
*/
@NeededForTesting
String getNumberForCall(ContactCacheEntry contactInfo) {
// If the name is empty, we use the number for the name...so don't show a second
// number in the number field
String preferredName = ContactDisplayUtils.getPreferredDisplayName(
contactInfo.namePrimary,
contactInfo.nameAlternative,
mContactsPreferences);
if (TextUtils.isEmpty(preferredName)) {
return contactInfo.location;
}
return contactInfo.number;
}
public void secondaryInfoClicked() {
if (mSecondary == null) {
Log.w(this, "Secondary info clicked but no secondary call.");
return;
}
Log.i(this, "Swapping call to foreground: " + mSecondary);
TelecomAdapter.getInstance().unholdCall(mSecondary.getId());
}
public void endCallClicked() {
if (mPrimary == null) {
return;
}
Log.i(this, "Disconnecting call: " + mPrimary);
final String callId = mPrimary.getId();
mPrimary.setState(Call.State.DISCONNECTING);
CallList.getInstance().onUpdate(mPrimary);
TelecomAdapter.getInstance().disconnectCall(callId);
}
private String getNumberFromHandle(Uri handle) {
return handle == null ? "" : handle.getSchemeSpecificPart();
}
/**
* Handles a change to the fullscreen mode of the in-call UI.
*
* @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode.
*/
@Override
public void onFullscreenModeChanged(boolean isFullscreenMode) {
mIsFullscreen = isFullscreenMode;
final CallCardUi ui = getUi();
if (ui == null) {
return;
}
ui.setCallCardVisible(!isFullscreenMode);
ui.setSecondaryInfoVisible(!isFullscreenMode);
maybeShowManageConferenceCallButton();
}
@Override
public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height) {
// No-op - the Call Card is the origin of this event.
}
private boolean isPrimaryCallActive() {
return mPrimary != null && mPrimary.getState() == Call.State.ACTIVE;
}
private String getConferenceString(Call call) {
boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
Log.v(this, "getConferenceString: " + isGenericConference);
final int resId = isGenericConference
? R.string.card_title_in_call : R.string.card_title_conf_call;
return mContext.getResources().getString(resId);
}
private Drawable getConferencePhoto(Call call) {
boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
Log.v(this, "getConferencePhoto: " + isGenericConference);
final int resId = isGenericConference
? R.drawable.img_phone : R.drawable.img_conference;
Drawable photo = mContext.getResources().getDrawable(resId);
photo.setAutoMirrored(true);
return photo;
}
private boolean shouldShowEndCallButton(Call primary, int callState) {
if (primary == null) {
return false;
}
if ((!Call.State.isConnectingOrConnected(callState)
&& callState != Call.State.DISCONNECTING) || callState == Call.State.INCOMING) {
return false;
}
if (mPrimary.getSessionModificationState()
== Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
return false;
}
return true;
}
private void maybeSendAccessibilityEvent(InCallState oldState, InCallState newState,
boolean primaryChanged) {
if (mContext == null) {
return;
}
final AccessibilityManager am = (AccessibilityManager) mContext.getSystemService(
Context.ACCESSIBILITY_SERVICE);
if (!am.isEnabled()) {
return;
}
// Announce the current call if it's new incoming/outgoing call or primary call is changed
// due to switching calls between two ongoing calls (one is on hold).
if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING)
|| (oldState != InCallState.INCOMING && newState == InCallState.INCOMING)
|| primaryChanged) {
if (getUi() != null) {
getUi().sendAccessibilityAnnouncement();
}
}
}
/**
* Determines whether the call subject should be visible on the UI. For the call subject to be
* visible, the call has to be in an incoming or waiting state, and the subject must not be
* empty.
*
* @param call The call.
* @return {@code true} if the subject should be shown, {@code false} otherwise.
*/
private boolean shouldShowCallSubject(Call call) {
if (call == null) {
return false;
}
boolean isIncomingOrWaiting = mPrimary.getState() == Call.State.INCOMING ||
mPrimary.getState() == Call.State.CALL_WAITING;
return isIncomingOrWaiting && !TextUtils.isEmpty(call.getCallSubject()) &&
call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED &&
call.isCallSubjectSupported();
}
/**
* Determines whether the "note sent" toast should be shown. It should be shown for a new
* outgoing call with a subject.
*
* @param call The call
* @return {@code true} if the toast should be shown, {@code false} otherwise.
*/
private boolean shouldShowNoteSentToast(Call call) {
return call != null && hasCallSubject(call) && (call.getState() == Call.State.DIALING
|| call.getState() == Call.State.CONNECTING);
}
private static boolean hasCallSubject(Call call) {
return !TextUtils.isEmpty(call.getTelecomCall().getDetails().getIntentExtras()
.getString(TelecomManager.EXTRA_CALL_SUBJECT));
}
public interface CallCardUi extends Ui {
void setVisible(boolean on);
void setContactContextTitle(View listHeaderView);
void setContactContextContent(ListAdapter listAdapter);
void showContactContext(boolean show);
void setCallCardVisible(boolean visible);
void setPrimary(String number, String name, boolean nameIsNumber, String label,
Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall);
void setSecondary(boolean show, String name, boolean nameIsNumber, String label,
String providerLabel, boolean isConference, boolean isVideoCall,
boolean isFullscreen);
void setSecondaryInfoVisible(boolean visible);
void setCallState(int state, int videoState, int sessionModificationState,
DisconnectCause disconnectCause, String connectionLabel,
Drawable connectionIcon, String gatewayNumber, boolean isWifi,
boolean isConference, boolean isWorkCall);
void setPrimaryCallElapsedTime(boolean show, long duration);
void setPrimaryName(String name, boolean nameIsNumber);
void setPrimaryImage(Drawable image, boolean isVisible);
void setPrimaryPhoneNumber(String phoneNumber);
void setPrimaryLabel(String label);
void setEndCallButtonEnabled(boolean enabled, boolean animate);
void setCallbackNumber(String number, boolean isEmergencyCalls);
void setCallSubject(String callSubject);
void setProgressSpinnerVisible(boolean visible);
void showHdAudioIndicator(boolean visible);
void showForwardIndicator(boolean visible);
void showManageConferenceCallButton(boolean visible);
boolean isManageConferenceVisible();
boolean isCallSubjectVisible();
void animateForNewOutgoingCall();
void sendAccessibilityAnnouncement();
void showNoteSentToast();
}
}