diff options
Diffstat (limited to 'java/com/android/incallui/CallCardPresenter.java')
-rw-r--r-- | java/com/android/incallui/CallCardPresenter.java | 1202 |
1 files changed, 1202 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..c2b99c1d1 --- /dev/null +++ b/java/com/android/incallui/CallCardPresenter.java @@ -0,0 +1,1202 @@ +/* + * 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.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.NonNull; +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.compat.ActivityCompat; +import com.android.dialer.enrichedcall.EnrichedCallComponent; +import com.android.dialer.enrichedcall.EnrichedCallManager; +import com.android.dialer.enrichedcall.Session; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; +import com.android.dialer.multimedia.MultimediaData; +import com.android.dialer.oem.MotorolaUtils; +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.DialerCallListener; +import com.android.incallui.calllocation.CallLocation; +import com.android.incallui.calllocation.CallLocationComponent; +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 com.android.incallui.videotech.utils.SessionModificationState; +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; + + @NonNull private final CallLocation callLocation; + 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(); + callLocation = CallLocationComponent.get(mContext).getCallLocation(); + } + + 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); + } + + // 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; + + // Log location impressions + if (isOutgoingEmergencyCall(mPrimary)) { + Logger.get(mContext).logImpression(DialerImpression.Type.EMERGENCY_NEW_EMERGENCY_CALL); + } else if (isIncomingEmergencyCall(mPrimary) || isIncomingEmergencyCall(mSecondary)) { + Logger.get(mContext).logImpression(DialerImpression.Type.EMERGENCY_CALLBACK); + } + + // Showing the location may have been skipped if the UI wasn't ready during previous layout. + if (shouldShowLocation()) { + updatePrimaryDisplayInfo(); + + // Log location impressions + if (!hasLocationPermission()) { + Logger.get(mContext).logImpression(DialerImpression.Type.EMERGENCY_NO_LOCATION_PERMISSION); + } else if (isBatteryTooLowForEmergencyLocation()) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.EMERGENCY_BATTERY_TOO_LOW_TO_GET_LOCATION); + } else if (!callLocation.canGetLocation(mContext)) { + Logger.get(mContext).logImpression(DialerImpression.Type.EMERGENCY_CANT_GET_LOCATION); + } + } + } + + @Override + public void onInCallScreenUnready() { + LogUtil.i("CallCardController.onInCallScreenUnready", null); + Assert.checkState(isInCallScreenReady); + + // 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); + } + + callLocation.close(); + + 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); + } + + 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); + } + + // 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() {} + + @Override + public void onInternationalCallOnWifi() {} + + /** 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. */ + @Override + public void onDialerCallSessionModificationStateChange() { + LogUtil.enterBlock("CallCardPresenter.onDialerCallSessionModificationStateChange"); + + if (mPrimary == null) { + return; + } + getUi() + .setEndCallButtonEnabled( + mPrimary.getVideoTech().getSessionModificationState() + != SessionModificationState.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); + boolean isAttemptingHdAudioCall = + !isHdAudioCall + && !mPrimary.hasProperty(DialerCall.PROPERTY_CODEC_KNOWN) + && MotorolaUtils.shouldBlinkHdIconWhenConnectingCall(mContext); + + boolean isBusiness = mPrimaryContactInfo != null && mPrimaryContactInfo.isBusiness; + + // 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.isVideoCall(), + mPrimary.getVideoTech().getSessionModificationState(), + mPrimary.getDisconnectCause(), + getConnectionLabel(), + getCallStateIcon(), + getGatewayNumber(), + shouldShowCallSubject(mPrimary) ? mPrimary.getCallSubject() : null, + mPrimary.getCallbackNumber(), + mPrimary.hasProperty(Details.PROPERTY_WIFI), + mPrimary.isConferenceCall() + && !mPrimary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE), + isWorkCall, + isAttemptingHdAudioCall, + isHdAudioCall, + !TextUtils.isEmpty(mPrimary.getLastForwardedNumber()), + shouldShowContactPhoto, + mPrimary.getConnectTimeMillis(), + CallerInfoUtils.isVoiceMailNumber(mContext, mPrimary), + mPrimary.isRemotelyHeld(), + isBusiness)); + + 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); + } + } + + /** 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); + + MultimediaData multimediaData = null; + if (mPrimary.getNumber() != null) { + EnrichedCallManager manager = EnrichedCallComponent.get(mContext).getEnrichedCallManager(); + + EnrichedCallManager.Filter filter; + if (mPrimary.isIncoming()) { + filter = manager.createIncomingCallComposerFilter(); + } else { + filter = manager.createOutgoingCallComposerFilter(); + } + + Session enrichedCallSession = + manager.getSession(mPrimary.getUniqueCallId(), mPrimary.getNumber(), filter); + + mPrimary.setEnrichedCallSession(enrichedCallSession); + mPrimary.setEnrichedCallCapabilities(manager.getCapabilities(mPrimary.getNumber())); + + if (enrichedCallSession != null) { + enrichedCallSession.setUniqueDialerCallId(mPrimary.getUniqueCallId()); + multimediaData = 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 */, + null /* photo */, + ContactPhotoType.DEFAULT_PLACEHOLDER, + false /* isSipCall */, + showContactPhoto, + hasWorkCallProperty, + false /* isSpam */, + false /* answeringDisconnectsOngoingCall */, + shouldShowLocation(), + null /* contactInfoLookupKey */, + null /* enrichedCallMultimediaData */, + mPrimary.getNumberPresentation())); + } 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, + mPrimary.updateNameIfRestricted(name), + nameIsNumber, + shouldShowLocationAsLabel(nameIsNumber, mPrimaryContactInfo.shouldShowLocation) + ? mPrimaryContactInfo.location + : null, + isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label, + mPrimaryContactInfo.photo, + mPrimaryContactInfo.photoType, + mPrimaryContactInfo.isSipCall, + showContactPhoto, + hasWorkCallProperty || isWorkContact, + mPrimary.isSpam(), + mPrimary.answeringDisconnectsForegroundVideoCall(), + shouldShowLocation(), + mPrimaryContactInfo.lookupKey, + multimediaData, + mPrimary.getNumberPresentation())); + } else { + // Clear the primary display info. + mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo()); + } + + if (isInCallScreenReady) { + mInCallScreen.showLocationUi(getLocationFragment()); + } else { + LogUtil.i("CallCardPresenter.updatePrimaryDisplayInfo", "UI not ready, not showing location"); + } + } + + private static boolean shouldShowLocationAsLabel( + boolean nameIsNumber, boolean shouldShowLocation) { + if (nameIsNumber) { + return true; + } + if (shouldShowLocation) { + return true; + } + return false; + } + + private Fragment getLocationFragment() { + if (!ConfigProviderBindings.get(mContext) + .getBoolean(CONFIG_ENABLE_EMERGENCY_LOCATION, CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT)) { + LogUtil.i("CallCardPresenter.getLocationFragment", "disabled by config."); + return null; + } + if (!shouldShowLocation()) { + LogUtil.i("CallCardPresenter.getLocationFragment", "shouldn't show location"); + return null; + } + if (!hasLocationPermission()) { + LogUtil.i("CallCardPresenter.getLocationFragment", "no location permission."); + return null; + } + if (isBatteryTooLowForEmergencyLocation()) { + LogUtil.i("CallCardPresenter.getLocationFragment", "low battery."); + return null; + } + if (ActivityCompat.isInMultiWindowMode(mInCallScreen.getInCallScreenFragment().getActivity())) { + LogUtil.i("CallCardPresenter.getLocationFragment", "in multi-window mode"); + return null; + } + if (mPrimary.isVideoCall()) { + LogUtil.i("CallCardPresenter.getLocationFragment", "emergency video calls not supported"); + return null; + } + if (!callLocation.canGetLocation(mContext)) { + LogUtil.i("CallCardPresenter.getLocationFragment", "can't get current location"); + return null; + } + LogUtil.i("CallCardPresenter.getLocationFragment", "returning location fragment"); + return callLocation.getLocationFragment(mContext); + } + + 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 */, + mSecondary.updateNameIfRestricted(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 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.getVideoTech().getSessionModificationState() + == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { + return false; + } + return true; + } + + @Override + public void onInCallScreenResumed() { + EnrichedCallComponent.get(mContext).getEnrichedCallManager().registerStateChangedListener(this); + updatePrimaryDisplayInfo(); + + if (shouldSendAccessibilityEvent) { + handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS); + } + } + + @Override + public void onInCallScreenPaused() { + EnrichedCallComponent.get(mContext) + .getEnrichedCallManager() + .unregisterStateChangedListener(this); + } + + 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); + } + } + } +} |