summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/CallCardPresenter.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/incallui/CallCardPresenter.java')
-rw-r--r--java/com/android/incallui/CallCardPresenter.java1110
1 files changed, 1110 insertions, 0 deletions
diff --git a/java/com/android/incallui/CallCardPresenter.java b/java/com/android/incallui/CallCardPresenter.java
new file mode 100644
index 000000000..930775772
--- /dev/null
+++ b/java/com/android/incallui/CallCardPresenter.java
@@ -0,0 +1,1110 @@
+/*
+ * 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 static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
+
+import android.Manifest;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.hardware.display.DisplayManager;
+import android.os.BatteryManager;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.content.ContextCompat;
+import android.telecom.Call.Details;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.view.Display;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.multimedia.MultimediaData;
+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.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCallListener;
+import com.android.incallui.incall.protocol.ContactPhotoType;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import java.lang.ref.WeakReference;
+
+/**
+ * Controller for the Call Card Fragment. This class listens for changes to InCallState and passes
+ * it along to the fragment.
+ */
+public class CallCardPresenter
+ implements InCallStateListener,
+ IncomingCallListener,
+ InCallDetailsListener,
+ InCallEventListener,
+ InCallScreenDelegate,
+ DialerCallListener,
+ EnrichedCallManager.StateChangedListener {
+
+ /**
+ * Amount of time to wait before sending an announcement via the accessibility manager. When the
+ * call state changes to an outgoing or incoming state for the first time, the UI can often be
+ * changing due to call updates or contact lookup. This allows the UI to settle to a stable state
+ * to ensure that the correct information is announced.
+ */
+ private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS = 500;
+
+ /** Flag to allow the user's current location to be shown during emergency calls. */
+ private static final String CONFIG_ENABLE_EMERGENCY_LOCATION = "config_enable_emergency_location";
+
+ private static final boolean CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT = true;
+
+ /**
+ * Make it possible to not get location during an emergency call if the battery is too low, since
+ * doing so could trigger gps and thus potentially cause the phone to die in the middle of the
+ * call.
+ */
+ private static final String CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION =
+ "min_battery_percent_for_emergency_location";
+
+ private static final long CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT = 10;
+
+ private final Context mContext;
+ private final Handler handler = new Handler();
+
+ private DialerCall mPrimary;
+ private DialerCall mSecondary;
+ private ContactCacheEntry mPrimaryContactInfo;
+ private ContactCacheEntry mSecondaryContactInfo;
+ @Nullable private ContactsPreferences mContactsPreferences;
+ private boolean mIsFullscreen = false;
+ private InCallScreen mInCallScreen;
+ private boolean isInCallScreenReady;
+ private boolean shouldSendAccessibilityEvent;
+ private final String locationModule = null;
+ private final Runnable sendAccessibilityEventRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ shouldSendAccessibilityEvent = !sendAccessibilityEvent(mContext, getUi());
+ LogUtil.i(
+ "CallCardPresenter.sendAccessibilityEventRunnable",
+ "still should send: %b",
+ shouldSendAccessibilityEvent);
+ if (!shouldSendAccessibilityEvent) {
+ handler.removeCallbacks(this);
+ }
+ }
+ };
+
+ public CallCardPresenter(Context context) {
+ LogUtil.i("CallCardController.constructor", null);
+ mContext = Assert.isNotNull(context).getApplicationContext();
+ }
+
+ private static boolean hasCallSubject(DialerCall call) {
+ return !TextUtils.isEmpty(call.getCallSubject());
+ }
+
+ @Override
+ public void onInCallScreenDelegateInit(InCallScreen inCallScreen) {
+ Assert.isNotNull(inCallScreen);
+ mInCallScreen = inCallScreen;
+ mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
+
+ // Call may be null if disconnect happened already.
+ DialerCall call = CallList.getInstance().getFirstCall();
+ if (call != null) {
+ mPrimary = call;
+ if (shouldShowNoteSentToast(mPrimary)) {
+ mInCallScreen.showNoteSentToast();
+ }
+ call.addListener(this);
+
+ // start processing lookups right away.
+ if (!call.isConferenceCall()) {
+ startContactInfoSearch(call, true, call.getState() == DialerCall.State.INCOMING);
+ } else {
+ updateContactEntry(null, true);
+ }
+ }
+
+ onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance());
+ }
+
+ @Override
+ public void onInCallScreenReady() {
+ LogUtil.i("CallCardController.onInCallScreenReady", null);
+ Assert.checkState(!isInCallScreenReady);
+ if (mContactsPreferences != null) {
+ mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
+ }
+
+ EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ .registerStateChangedListener(this);
+
+ // 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);
+ isInCallScreenReady = true;
+ }
+
+ @Override
+ public void onInCallScreenUnready() {
+ LogUtil.i("CallCardController.onInCallScreenUnready", null);
+ Assert.checkState(isInCallScreenReady);
+
+ EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ .unregisterStateChangedListener(this);
+ // stop getting call state changes
+ InCallPresenter.getInstance().removeListener(this);
+ InCallPresenter.getInstance().removeIncomingCallListener(this);
+ InCallPresenter.getInstance().removeDetailsListener(this);
+ InCallPresenter.getInstance().removeInCallEventListener(this);
+ if (mPrimary != null) {
+ mPrimary.removeListener(this);
+ }
+
+ mPrimary = null;
+ mPrimaryContactInfo = null;
+ mSecondaryContactInfo = null;
+ isInCallScreenReady = false;
+ }
+
+ @Override
+ public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
+ // same logic should happen as with onStateChange()
+ onStateChange(oldState, newState, CallList.getInstance());
+ }
+
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ LogUtil.v("CallCardPresenter.onStateChange", "" + newState);
+ if (mInCallScreen == null) {
+ return;
+ }
+
+ DialerCall primary = null;
+ DialerCall 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);
+ }
+
+ LogUtil.v("CallCardPresenter.onStateChange", "primary call: " + primary);
+ LogUtil.v("CallCardPresenter.onStateChange", "secondary call: " + secondary);
+
+ final boolean primaryChanged =
+ !(DialerCall.areSame(mPrimary, primary) && DialerCall.areSameNumber(mPrimary, primary));
+ final boolean secondaryChanged =
+ !(DialerCall.areSame(mSecondary, secondary)
+ && DialerCall.areSameNumber(mSecondary, secondary));
+
+ mSecondary = secondary;
+ DialerCall previousPrimary = mPrimary;
+ mPrimary = primary;
+
+ if (mPrimary != null) {
+ InCallPresenter.getInstance().onForegroundCallChanged(mPrimary);
+ mInCallScreen.updateInCallScreenColors();
+ }
+
+ if (primaryChanged && shouldShowNoteSentToast(primary)) {
+ mInCallScreen.showNoteSentToast();
+ }
+
+ // Refresh primary call information if either:
+ // 1. Primary call changed.
+ // 2. The call's ability to manage conference has changed.
+ if (shouldRefreshPrimaryInfo(primaryChanged)) {
+ // primary call has changed
+ if (previousPrimary != null) {
+ previousPrimary.removeListener(this);
+ }
+ mPrimary.addListener(this);
+
+ mPrimaryContactInfo =
+ ContactInfoCache.buildCacheEntryFromCall(
+ mContext, mPrimary, mPrimary.getState() == DialerCall.State.INCOMING);
+ updatePrimaryDisplayInfo();
+ maybeStartSearch(mPrimary, true);
+ maybeClearSessionModificationState(mPrimary);
+ }
+
+ if (previousPrimary != null && mPrimary == null) {
+ previousPrimary.removeListener(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() == DialerCall.State.INCOMING);
+ updateSecondaryDisplayInfo();
+ maybeStartSearch(mSecondary, false);
+ maybeClearSessionModificationState(mSecondary);
+ }
+
+ // Set the call state
+ int callState = DialerCall.State.IDLE;
+ if (mPrimary != null) {
+ callState = mPrimary.getState();
+ updatePrimaryCallState();
+ } else {
+ getUi().setCallState(PrimaryCallState.createEmptyPrimaryCallState());
+ }
+
+ maybeShowManageConferenceCallButton();
+
+ // Hide the end call button instantly if we're receiving an incoming call.
+ getUi()
+ .setEndCallButtonEnabled(
+ shouldShowEndCallButton(mPrimary, callState),
+ callState != DialerCall.State.INCOMING /* animate */);
+
+ maybeSendAccessibilityEvent(oldState, newState, primaryChanged);
+ }
+
+ @Override
+ public void onDetailsChanged(DialerCall call, Details details) {
+ updatePrimaryCallState();
+
+ if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE)
+ != details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) {
+ maybeShowManageConferenceCallButton();
+ }
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {}
+
+ @Override
+ public void onDialerCallUpdate() {
+ // No-op; specific call updates handled elsewhere.
+ }
+
+ @Override
+ public void onWiFiToLteHandover() {}
+
+ @Override
+ public void onHandoverToWifiFailure() {}
+
+ /** Handles a change to the child number by refreshing the primary call info. */
+ @Override
+ public void onDialerCallChildNumberChange() {
+ LogUtil.v("CallCardPresenter.onDialerCallChildNumberChange", "");
+
+ if (mPrimary == null) {
+ return;
+ }
+ updatePrimaryDisplayInfo();
+ }
+
+ /** Handles a change to the last forwarding number by refreshing the primary call info. */
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {
+ LogUtil.v("CallCardPresenter.onDialerCallLastForwardedNumberChange", "");
+
+ if (mPrimary == null) {
+ return;
+ }
+ updatePrimaryDisplayInfo();
+ updatePrimaryCallState();
+ }
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {}
+
+ /**
+ * Handles a change to the session modification state for a call.
+ *
+ * @param sessionModificationState The new session modification state.
+ */
+ @Override
+ public void onDialerCallSessionModificationStateChange(
+ @SessionModificationState int sessionModificationState) {
+ LogUtil.v(
+ "CallCardPresenter.onDialerCallSessionModificationStateChange",
+ "state: " + sessionModificationState);
+
+ if (mPrimary == null) {
+ return;
+ }
+ getUi()
+ .setEndCallButtonEnabled(
+ sessionModificationState
+ != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+ true /* shouldAnimate */);
+ updatePrimaryCallState();
+ }
+
+ @Override
+ public void onEnrichedCallStateChanged() {
+ LogUtil.enterBlock("CallCardPresenter.onEnrichedCallStateChanged");
+ updatePrimaryDisplayInfo();
+ }
+
+ private boolean shouldRefreshPrimaryInfo(boolean primaryChanged) {
+ if (mPrimary == null) {
+ return false;
+ }
+ return primaryChanged
+ || mInCallScreen.isManageConferenceVisible() != shouldShowManageConference();
+ }
+
+ private void updatePrimaryCallState() {
+ if (getUi() != null && mPrimary != null) {
+ boolean isWorkCall =
+ mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL)
+ || (mPrimaryContactInfo != null
+ && mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
+ boolean isHdAudioCall =
+ isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO);
+ // Check for video state change and update the visibility of the contact photo. The contact
+ // photo is hidden when the incoming video surface is shown.
+ // The contact photo visibility can also change in setPrimary().
+ boolean shouldShowContactPhoto =
+ !VideoCallPresenter.showIncomingVideo(mPrimary.getVideoState(), mPrimary.getState());
+ getUi()
+ .setCallState(
+ new PrimaryCallState(
+ mPrimary.getState(),
+ mPrimary.getVideoState(),
+ mPrimary.getSessionModificationState(),
+ mPrimary.getDisconnectCause(),
+ getConnectionLabel(),
+ getCallStateIcon(),
+ getGatewayNumber(),
+ shouldShowCallSubject(mPrimary) ? mPrimary.getCallSubject() : null,
+ mPrimary.getCallbackNumber(),
+ mPrimary.hasProperty(Details.PROPERTY_WIFI),
+ mPrimary.isConferenceCall(),
+ isWorkCall,
+ isHdAudioCall,
+ !TextUtils.isEmpty(mPrimary.getLastForwardedNumber()),
+ shouldShowContactPhoto,
+ mPrimary.getConnectTimeMillis(),
+ CallerInfoUtils.isVoiceMailNumber(mContext, mPrimary),
+ mPrimary.isRemotelyHeld()));
+
+ InCallActivity activity =
+ (InCallActivity) (mInCallScreen.getInCallScreenFragment().getActivity());
+ if (activity != null) {
+ activity.onPrimaryCallStateChanged();
+ }
+ }
+ }
+
+ /** Only show the conference call button if we can manage the conference. */
+ private void maybeShowManageConferenceCallButton() {
+ getUi().showManageConferenceCallButton(shouldShowManageConference());
+ }
+
+ /**
+ * 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;
+ }
+
+ @Override
+ public void onCallStateButtonClicked() {
+ Intent broadcastIntent = Bindings.get(mContext).getCallStateButtonBroadcastIntent(mContext);
+ if (broadcastIntent != null) {
+ LogUtil.v(
+ "CallCardPresenter.onCallStateButtonClicked",
+ "sending call state button broadcast: " + broadcastIntent);
+ mContext.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE);
+ }
+ }
+
+ @Override
+ public void onManageConferenceClicked() {
+ InCallActivity activity =
+ (InCallActivity) (mInCallScreen.getInCallScreenFragment().getActivity());
+ activity.showConferenceFragment(true);
+ }
+
+ @Override
+ public void onShrinkAnimationComplete() {
+ InCallPresenter.getInstance().onShrinkAnimationComplete();
+ }
+
+ @Override
+ public Drawable getDefaultContactPhotoDrawable() {
+ return ContactInfoCache.getInstance(mContext).getDefaultContactPhotoDrawable();
+ }
+
+ private void maybeStartSearch(DialerCall 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() == DialerCall.State.INCOMING);
+ }
+ }
+
+ private void maybeClearSessionModificationState(DialerCall call) {
+ @SessionModificationState int state = call.getSessionModificationState();
+ if (state != DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST
+ && state != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ LogUtil.i("CallCardPresenter.maybeClearSessionModificationState", "clearing state");
+ call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ }
+
+ /** Starts a query for more contact data for the save primary and secondary calls. */
+ private void startContactInfoSearch(
+ final DialerCall 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 {
+ LogUtil.e(
+ "CallCardPresenter.onContactInfoComplete",
+ "dropping stale contact lookup info for " + callId);
+ }
+
+ final DialerCall 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())) {
+ updateContactEntry(entry, true /* isPrimary */);
+ } else if (mSecondary != null && callId.equals(mSecondary.getId())) {
+ updateContactEntry(entry, false /* isPrimary */);
+ }
+ }
+ }
+
+ 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 DialerCall getCallToDisplay(
+ CallList callList, DialerCall ignore, boolean skipDisconnected) {
+ // Active calls come second. An active call always gets precedent.
+ DialerCall 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() {
+ if (mInCallScreen == null) {
+ // TODO: May also occur if search result comes back after ui is destroyed. Look into
+ // removing that case completely.
+ LogUtil.v(
+ "CallCardPresenter.updatePrimaryDisplayInfo",
+ "updatePrimaryDisplayInfo called but ui is null!");
+ return;
+ }
+
+ if (mPrimary == null) {
+ // Clear the primary display info.
+ mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo());
+ 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());
+
+ // DialerCall placed through a work phone account.
+ boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL);
+
+ Session enrichedCallSession =
+ mPrimary.getNumber() == null
+ ? null
+ : EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ .getSession(mPrimary.getNumber());
+ MultimediaData enrichedCallMultimediaData =
+ enrichedCallSession == null ? null : enrichedCallSession.getMultimediaData();
+
+ if (mPrimary.isConferenceCall()) {
+ LogUtil.v(
+ "CallCardPresenter.updatePrimaryDisplayInfo",
+ "update primary display info for conference call.");
+
+ mInCallScreen.setPrimary(
+ new PrimaryInfo(
+ null /* number */,
+ getConferenceString(mPrimary),
+ false /* nameIsNumber */,
+ null /* location */,
+ null /* label */,
+ getConferencePhoto(mPrimary),
+ ContactPhotoType.DEFAULT_PLACEHOLDER,
+ false /* isSipCall */,
+ showContactPhoto,
+ hasWorkCallProperty,
+ false /* isSpam */,
+ false /* answeringDisconnectsOngoingCall */,
+ shouldShowLocation(),
+ null /* contactInfoLookupKey */,
+ null /* enrichedCallMultimediaData */));
+ } else if (mPrimaryContactInfo != null) {
+ LogUtil.v(
+ "CallCardPresenter.updatePrimaryDisplayInfo",
+ "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) {
+ 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 = mPrimaryContactInfo.number;
+ }
+
+ boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
+ // DialerCall with caller that is a work contact.
+ boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
+ mInCallScreen.setPrimary(
+ new PrimaryInfo(
+ number,
+ name,
+ nameIsNumber,
+ mPrimaryContactInfo.location,
+ isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label,
+ mPrimaryContactInfo.photo,
+ mPrimaryContactInfo.photoType,
+ mPrimaryContactInfo.isSipCall,
+ showContactPhoto,
+ hasWorkCallProperty || isWorkContact,
+ mPrimary.isSpam(),
+ mPrimary.answeringDisconnectsForegroundVideoCall(),
+ shouldShowLocation(),
+ mPrimaryContactInfo.lookupKey,
+ enrichedCallMultimediaData));
+ } else {
+ // Clear the primary display info.
+ mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo());
+ }
+
+ mInCallScreen.showLocationUi(null);
+ }
+
+ private boolean shouldShowLocation() {
+ if (isOutgoingEmergencyCall(mPrimary)) {
+ LogUtil.i("CallCardPresenter.shouldShowLocation", "new emergency call");
+ return true;
+ } else if (isIncomingEmergencyCall(mPrimary)) {
+ LogUtil.i("CallCardPresenter.shouldShowLocation", "potential emergency callback");
+ return true;
+ } else if (isIncomingEmergencyCall(mSecondary)) {
+ LogUtil.i("CallCardPresenter.shouldShowLocation", "has potential emergency callback");
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean isOutgoingEmergencyCall(@Nullable DialerCall call) {
+ return call != null && !call.isIncoming() && call.isEmergencyCall();
+ }
+
+ private static boolean isIncomingEmergencyCall(@Nullable DialerCall call) {
+ return call != null && call.isIncoming() && call.isPotentialEmergencyCallback();
+ }
+
+ private boolean hasLocationPermission() {
+ return ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private boolean isBatteryTooLowForEmergencyLocation() {
+ Intent batteryStatus =
+ mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+ if (status == BatteryManager.BATTERY_STATUS_CHARGING
+ || status == BatteryManager.BATTERY_STATUS_FULL) {
+ // Plugged in or full battery
+ return false;
+ }
+ int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ float batteryPercent = (100f * level) / scale;
+ long threshold =
+ ConfigProviderBindings.get(mContext)
+ .getLong(
+ CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION,
+ CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT);
+ LogUtil.i(
+ "CallCardPresenter.isBatteryTooLowForEmergencyLocation",
+ "percent charged: " + batteryPercent + ", min required charge: " + threshold);
+ return batteryPercent < threshold;
+ }
+
+ private void updateSecondaryDisplayInfo() {
+ if (mInCallScreen == null) {
+ return;
+ }
+
+ if (mSecondary == null) {
+ // Clear the secondary display info.
+ mInCallScreen.setSecondary(SecondaryInfo.createEmptySecondaryInfo(mIsFullscreen));
+ return;
+ }
+
+ if (mSecondary.isConferenceCall()) {
+ mInCallScreen.setSecondary(
+ new SecondaryInfo(
+ true /* show */,
+ getConferenceString(mSecondary),
+ false /* nameIsNumber */,
+ null /* label */,
+ mSecondary.getCallProviderLabel(),
+ true /* isConference */,
+ mSecondary.isVideoCall(),
+ mIsFullscreen));
+ } else if (mSecondaryContactInfo != null) {
+ LogUtil.v("CallCardPresenter.updateSecondaryDisplayInfo", "" + mSecondaryContactInfo);
+ String name = getNameForCall(mSecondaryContactInfo);
+ boolean nameIsNumber = name != null && name.equals(mSecondaryContactInfo.number);
+ mInCallScreen.setSecondary(
+ new SecondaryInfo(
+ true /* show */,
+ name,
+ nameIsNumber,
+ mSecondaryContactInfo.label,
+ mSecondary.getCallProviderLabel(),
+ false /* isConference */,
+ mSecondary.isVideoCall(),
+ mIsFullscreen));
+ } else {
+ // Clear the secondary display info.
+ mInCallScreen.setSecondary(SecondaryInfo.createEmptySecondaryInfo(mIsFullscreen));
+ }
+ }
+
+ /** Returns the gateway number for any existing outgoing call. */
+ private String getGatewayNumber() {
+ if (hasOutgoingGatewayCall()) {
+ return DialerCall.getNumberFromHandle(mPrimary.getGatewayInfo().getGatewayAddress());
+ }
+ 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() {
+ if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED) {
+ return null;
+ }
+ StatusHints statusHints = mPrimary.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) {
+ LogUtil.e("CallCardPresenter.getConnectionLabel", "gateway Application Not Found.", e);
+ return null;
+ }
+ }
+ return mPrimary.getCallProviderLabel();
+ }
+
+ private Drawable getCallStateIcon() {
+ // Return connection icon if one exists.
+ StatusHints statusHints = mPrimary.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 DialerCall.State.isDialing(mPrimary.getState())
+ && mPrimary.getGatewayInfo() != null
+ && !mPrimary.getGatewayInfo().isEmpty();
+ }
+
+ /** Gets the name to display for the call. */
+ 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. */
+ 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;
+ }
+
+ @Override
+ public void onSecondaryInfoClicked() {
+ if (mSecondary == null) {
+ LogUtil.e(
+ "CallCardPresenter.onSecondaryInfoClicked",
+ "secondary info clicked but no secondary call.");
+ return;
+ }
+
+ LogUtil.i(
+ "CallCardPresenter.onSecondaryInfoClicked", "swapping call to foreground: " + mSecondary);
+ mSecondary.unhold();
+ }
+
+ @Override
+ public void onEndCallClicked() {
+ LogUtil.i("CallCardPresenter.onEndCallClicked", "disconnecting call: " + mPrimary);
+ if (mPrimary != null) {
+ mPrimary.disconnect();
+ }
+ }
+
+ /**
+ * 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;
+ if (mInCallScreen == null) {
+ return;
+ }
+ maybeShowManageConferenceCallButton();
+ }
+
+ private boolean isPrimaryCallActive() {
+ return mPrimary != null && mPrimary.getState() == DialerCall.State.ACTIVE;
+ }
+
+ private String getConferenceString(DialerCall call) {
+ boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
+ LogUtil.v("CallCardPresenter.getConferenceString", "" + isGenericConference);
+
+ final int resId =
+ isGenericConference ? R.string.generic_conference_call_name : R.string.conference_call_name;
+ return mContext.getResources().getString(resId);
+ }
+
+ private Drawable getConferencePhoto(DialerCall call) {
+ boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
+ LogUtil.v("CallCardPresenter.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(DialerCall primary, int callState) {
+ if (primary == null) {
+ return false;
+ }
+ if ((!DialerCall.State.isConnectingOrConnected(callState)
+ && callState != DialerCall.State.DISCONNECTING
+ && callState != DialerCall.State.DISCONNECTED)
+ || callState == DialerCall.State.INCOMING) {
+ return false;
+ }
+ if (mPrimary.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onInCallScreenResumed() {
+ if (shouldSendAccessibilityEvent) {
+ handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS);
+ }
+ }
+
+ static boolean sendAccessibilityEvent(Context context, InCallScreen inCallScreen) {
+ AccessibilityManager am =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (!am.isEnabled()) {
+ LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "accessibility is off");
+ return false;
+ }
+ if (inCallScreen == null) {
+ LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "incallscreen is null");
+ return false;
+ }
+ Fragment fragment = inCallScreen.getInCallScreenFragment();
+ if (fragment == null || fragment.getView() == null || fragment.getView().getParent() == null) {
+ LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "fragment/view/parent is null");
+ return false;
+ }
+
+ DisplayManager displayManager =
+ (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ boolean screenIsOn = display.getState() == Display.STATE_ON;
+ LogUtil.d("CallCardPresenter.sendAccessibilityEvent", "screen is on: %b", screenIsOn);
+ if (!screenIsOn) {
+ return false;
+ }
+
+ AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ inCallScreen.dispatchPopulateAccessibilityEvent(event);
+ View view = inCallScreen.getInCallScreenFragment().getView();
+ view.getParent().requestSendAccessibilityEvent(view, event);
+ return true;
+ }
+
+ private void maybeSendAccessibilityEvent(
+ InCallState oldState, final InCallState newState, boolean primaryChanged) {
+ shouldSendAccessibilityEvent = false;
+ 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) {
+ LogUtil.i(
+ "CallCardPresenter.maybeSendAccessibilityEvent", "schedule accessibility announcement");
+ shouldSendAccessibilityEvent = true;
+ handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS);
+ }
+ }
+
+ /**
+ * 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(DialerCall call) {
+ if (call == null) {
+ return false;
+ }
+
+ boolean isIncomingOrWaiting =
+ mPrimary.getState() == DialerCall.State.INCOMING
+ || mPrimary.getState() == DialerCall.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(DialerCall call) {
+ return call != null
+ && hasCallSubject(call)
+ && (call.getState() == DialerCall.State.DIALING
+ || call.getState() == DialerCall.State.CONNECTING);
+ }
+
+ private InCallScreen getUi() {
+ return mInCallScreen;
+ }
+
+ public static class ContactLookupCallback implements ContactInfoCacheCallback {
+
+ private final WeakReference<CallCardPresenter> mCallCardPresenter;
+ private final boolean mIsPrimary;
+
+ public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) {
+ mCallCardPresenter = new WeakReference<CallCardPresenter>(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);
+ }
+ }
+ }
+}