| /* |
| * Copyright (C) 2016 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.answer.impl; |
| |
| import android.Manifest.permission; |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.annotation.SuppressLint; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.location.Location; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Trace; |
| import android.support.annotation.DrawableRes; |
| import android.support.annotation.FloatRange; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.StringRes; |
| import android.support.annotation.VisibleForTesting; |
| import android.support.v4.app.Fragment; |
| import android.text.TextUtils; |
| import android.transition.TransitionManager; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.AccessibilityDelegate; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup; |
| import android.view.ViewTreeObserver.OnGlobalLayoutListener; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| import com.android.dialer.common.Assert; |
| import com.android.dialer.common.FragmentUtils; |
| import com.android.dialer.common.LogUtil; |
| import com.android.dialer.common.MathUtil; |
| import com.android.dialer.logging.DialerImpression.Type; |
| import com.android.dialer.logging.Logger; |
| import com.android.dialer.multimedia.MultimediaData; |
| import com.android.dialer.telecom.TelecomUtil; |
| import com.android.dialer.util.ViewUtil; |
| import com.android.incallui.answer.impl.CreateCustomSmsDialogFragment.CreateCustomSmsHolder; |
| import com.android.incallui.answer.impl.SmsBottomSheetFragment.SmsSheetHolder; |
| import com.android.incallui.answer.impl.affordance.SwipeButtonHelper.Callback; |
| import com.android.incallui.answer.impl.affordance.SwipeButtonView; |
| import com.android.incallui.answer.impl.answermethod.AnswerMethod; |
| import com.android.incallui.answer.impl.answermethod.AnswerMethodFactory; |
| import com.android.incallui.answer.impl.answermethod.AnswerMethodHolder; |
| import com.android.incallui.answer.impl.utils.Interpolators; |
| import com.android.incallui.answer.protocol.AnswerScreen; |
| import com.android.incallui.answer.protocol.AnswerScreenDelegate; |
| import com.android.incallui.answer.protocol.AnswerScreenDelegateFactory; |
| import com.android.incallui.call.state.DialerCallState; |
| import com.android.incallui.contactgrid.ContactGridManager; |
| 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.InCallScreenDelegateFactory; |
| 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.incalluilock.InCallUiLock; |
| import com.android.incallui.maps.MapsComponent; |
| import com.android.incallui.sessiondata.AvatarPresenter; |
| import com.android.incallui.sessiondata.MultimediaFragment; |
| import com.android.incallui.sessiondata.MultimediaFragment.Holder; |
| import com.android.incallui.speakeasy.SpeakEasyComponent; |
| import com.android.incallui.util.AccessibilityUtil; |
| import com.android.incallui.video.protocol.VideoCallScreen; |
| import com.android.incallui.videotech.utils.VideoUtils; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Optional; |
| |
| /** The new version of the incoming call screen. */ |
| @SuppressLint("ClickableViewAccessibility") |
| public class AnswerFragment extends Fragment |
| implements AnswerScreen, |
| InCallScreen, |
| SmsSheetHolder, |
| CreateCustomSmsHolder, |
| AnswerMethodHolder, |
| Holder { |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) |
| static final String ARG_CALL_ID = "call_id"; |
| |
| static final String ARG_IS_RTT_CALL = "is_rtt_call"; |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) |
| static final String ARG_IS_VIDEO_CALL = "is_video_call"; |
| |
| static final String ARG_ALLOW_ANSWER_AND_RELEASE = "allow_answer_and_release"; |
| |
| static final String ARG_HAS_CALL_ON_HOLD = "has_call_on_hold"; |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) |
| static final String ARG_IS_VIDEO_UPGRADE_REQUEST = "is_video_upgrade_request"; |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) |
| static final String ARG_IS_SELF_MANAGED_CAMERA = "is_self_managed_camera"; |
| |
| static final String ARG_ALLOW_SPEAK_EASY = "allow_speak_easy"; |
| |
| private static final String STATE_HAS_ANIMATED_ENTRY = "hasAnimated"; |
| |
| private static final int HINT_SECONDARY_SHOW_DURATION_MILLIS = 5000; |
| private static final float ANIMATE_LERP_PROGRESS = 0.5f; |
| private static final int STATUS_BAR_DISABLE_RECENT = 0x01000000; |
| private static final int STATUS_BAR_DISABLE_HOME = 0x00200000; |
| private static final int STATUS_BAR_DISABLE_BACK = 0x00400000; |
| |
| private static void fadeToward(View view, float newAlpha) { |
| view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, ANIMATE_LERP_PROGRESS)); |
| } |
| |
| private static void scaleToward(View view, float newScale) { |
| view.setScaleX(MathUtil.lerp(view.getScaleX(), newScale, ANIMATE_LERP_PROGRESS)); |
| view.setScaleY(MathUtil.lerp(view.getScaleY(), newScale, ANIMATE_LERP_PROGRESS)); |
| } |
| |
| private AnswerScreenDelegate answerScreenDelegate; |
| private InCallScreenDelegate inCallScreenDelegate; |
| |
| private View importanceBadge; |
| private SwipeButtonView secondaryButton; |
| private SwipeButtonView answerAndReleaseButton; |
| private AffordanceHolderLayout affordanceHolderLayout; |
| private LinearLayout chipContainer; |
| // Use these flags to prevent user from clicking accept/reject buttons multiple times. |
| // We use separate flags because in some rare cases accepting a call may fail to join the room, |
| // and then user is stuck in the incoming call view until it times out. Two flags at least give |
| // the user a chance to get out of the CallActivity. |
| private boolean buttonAcceptClicked; |
| private boolean buttonRejectClicked; |
| private boolean hasAnimatedEntry; |
| private PrimaryInfo primaryInfo = PrimaryInfo.empty(); |
| private PrimaryCallState primaryCallState; |
| private ArrayList<CharSequence> textResponses; |
| private SmsBottomSheetFragment textResponsesFragment; |
| private CreateCustomSmsDialogFragment createCustomSmsDialogFragment; |
| private SecondaryBehavior secondaryBehavior = SecondaryBehavior.REJECT_WITH_SMS; |
| private SecondaryBehavior answerAndReleaseBehavior; |
| private ContactGridManager contactGridManager; |
| private VideoCallScreen answerVideoCallScreen; |
| private Handler handler = new Handler(Looper.getMainLooper()); |
| |
| private enum SecondaryBehavior { |
| REJECT_WITH_SMS( |
| R.drawable.quantum_ic_message_white_24, |
| R.string.a11y_description_incoming_call_reject_with_sms, |
| R.string.a11y_incoming_call_reject_with_sms, |
| R.string.call_incoming_swipe_to_decline_with_message) { |
| @Override |
| public void performAction(AnswerFragment fragment) { |
| fragment.showMessageMenu(); |
| } |
| }, |
| |
| ANSWER_VIDEO_AS_AUDIO( |
| R.drawable.quantum_ic_videocam_off_vd_theme_24, |
| R.string.a11y_description_incoming_call_answer_video_as_audio, |
| R.string.a11y_incoming_call_answer_video_as_audio, |
| R.string.call_incoming_swipe_to_answer_video_as_audio) { |
| @Override |
| public void performAction(AnswerFragment fragment) { |
| fragment.acceptCallByUser(true /* answerVideoAsAudio */); |
| } |
| }, |
| |
| ANSWER_AND_RELEASE( |
| R.drawable.ic_end_answer_32, |
| R.string.a11y_description_incoming_call_answer_and_release, |
| R.string.a11y_incoming_call_answer_and_release, |
| R.string.call_incoming_swipe_to_answer_and_release) { |
| @Override |
| public void performAction(AnswerFragment fragment) { |
| fragment.performAnswerAndRelease(); |
| } |
| }; |
| |
| @DrawableRes public int icon; |
| @StringRes public final int contentDescription; |
| @StringRes public final int accessibilityLabel; |
| @StringRes public final int hintText; |
| |
| SecondaryBehavior( |
| @DrawableRes int icon, |
| @StringRes int contentDescription, |
| @StringRes int accessibilityLabel, |
| @StringRes int hintText) { |
| this.icon = icon; |
| this.contentDescription = contentDescription; |
| this.accessibilityLabel = accessibilityLabel; |
| this.hintText = hintText; |
| } |
| |
| public abstract void performAction(AnswerFragment fragment); |
| |
| public void applyToView(ImageView view) { |
| view.setImageResource(icon); |
| view.setContentDescription(view.getContext().getText(contentDescription)); |
| } |
| } |
| |
| private void performSpeakEasy(View unused) { |
| answerScreenDelegate.onSpeakEasyCall(); |
| buttonAcceptClicked = true; |
| } |
| |
| private void performAnswerAndRelease() { |
| restoreAnswerAndReleaseButtonAnimation(); |
| answerScreenDelegate.onAnswerAndReleaseCall(); |
| buttonAcceptClicked = true; |
| } |
| |
| private void restoreAnswerAndReleaseButtonAnimation() { |
| answerAndReleaseButton |
| .animate() |
| .alpha(0) |
| .withEndAction( |
| new Runnable() { |
| @Override |
| public void run() { |
| affordanceHolderLayout.reset(false); |
| secondaryButton.animate().alpha(1); |
| } |
| }); |
| } |
| |
| private final AccessibilityDelegate accessibilityDelegate = |
| new AccessibilityDelegate() { |
| @Override |
| public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(host, info); |
| if (host == secondaryButton) { |
| CharSequence label = getText(secondaryBehavior.accessibilityLabel); |
| info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label)); |
| } else if (host == answerAndReleaseButton) { |
| CharSequence label = getText(answerAndReleaseBehavior.accessibilityLabel); |
| info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label)); |
| } |
| } |
| |
| @Override |
| public boolean performAccessibilityAction(View host, int action, Bundle args) { |
| if (action == AccessibilityNodeInfo.ACTION_CLICK) { |
| if (host == secondaryButton) { |
| performSecondaryButtonAction(); |
| return true; |
| } else if (host == answerAndReleaseButton) { |
| performAnswerAndReleaseButtonAction(); |
| return true; |
| } |
| } |
| return super.performAccessibilityAction(host, action, args); |
| } |
| }; |
| |
| private final Callback affordanceCallback = |
| new Callback() { |
| @Override |
| public void onAnimationToSideStarted(boolean rightPage, float translation, float vel) {} |
| |
| @Override |
| public void onAnimationToSideEnded(boolean rightPage) { |
| if (rightPage) { |
| performAnswerAndReleaseButtonAction(); |
| } else { |
| performSecondaryButtonAction(); |
| } |
| } |
| |
| @Override |
| public float getMaxTranslationDistance() { |
| View view = getView(); |
| if (view == null) { |
| return 0; |
| } |
| return (float) Math.hypot(view.getWidth(), view.getHeight()); |
| } |
| |
| @Override |
| public void onSwipingStarted(boolean rightIcon) {} |
| |
| @Override |
| public void onSwipingAborted() {} |
| |
| @Override |
| public void onIconClicked(boolean rightIcon) { |
| affordanceHolderLayout.startHintAnimation(rightIcon, null); |
| getAnswerMethod() |
| .setHintText( |
| rightIcon |
| ? getText(answerAndReleaseBehavior.hintText) |
| : getText(secondaryBehavior.hintText)); |
| handler.removeCallbacks(swipeHintRestoreTimer); |
| handler.postDelayed(swipeHintRestoreTimer, HINT_SECONDARY_SHOW_DURATION_MILLIS); |
| } |
| |
| @Override |
| public SwipeButtonView getLeftIcon() { |
| return secondaryButton; |
| } |
| |
| @Override |
| public SwipeButtonView getRightIcon() { |
| return answerAndReleaseButton; |
| } |
| |
| @Override |
| public View getLeftPreview() { |
| return null; |
| } |
| |
| @Override |
| public View getRightPreview() { |
| return null; |
| } |
| |
| @Override |
| public float getAffordanceFalsingFactor() { |
| return 1.0f; |
| } |
| }; |
| |
| private Runnable swipeHintRestoreTimer = this::restoreSwipeHintTexts; |
| |
| private void performSecondaryButtonAction() { |
| secondaryBehavior.performAction(this); |
| } |
| |
| private void performAnswerAndReleaseButtonAction() { |
| answerAndReleaseBehavior.performAction(this); |
| } |
| |
| public static AnswerFragment newInstance( |
| String callId, |
| boolean isRttCall, |
| boolean isVideoCall, |
| boolean isVideoUpgradeRequest, |
| boolean isSelfManagedCamera, |
| boolean allowAnswerAndRelease, |
| boolean hasCallOnHold, |
| boolean allowSpeakEasy) { |
| Bundle bundle = new Bundle(); |
| bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId)); |
| bundle.putBoolean(ARG_IS_RTT_CALL, isRttCall); |
| bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall); |
| bundle.putBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST, isVideoUpgradeRequest); |
| bundle.putBoolean(ARG_IS_SELF_MANAGED_CAMERA, isSelfManagedCamera); |
| bundle.putBoolean(ARG_ALLOW_ANSWER_AND_RELEASE, allowAnswerAndRelease); |
| bundle.putBoolean(ARG_HAS_CALL_ON_HOLD, hasCallOnHold); |
| bundle.putBoolean(ARG_ALLOW_SPEAK_EASY, allowSpeakEasy); |
| |
| AnswerFragment instance = new AnswerFragment(); |
| instance.setArguments(bundle); |
| return instance; |
| } |
| |
| @Override |
| public boolean isActionTimeout() { |
| return (buttonAcceptClicked || buttonRejectClicked) && answerScreenDelegate.isActionTimeout(); |
| } |
| |
| @Override |
| @NonNull |
| public String getCallId() { |
| return Assert.isNotNull(getArguments().getString(ARG_CALL_ID)); |
| } |
| |
| @Override |
| public boolean isVideoUpgradeRequest() { |
| return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST); |
| } |
| |
| @Override |
| public void setTextResponses(List<String> textResponses) { |
| if (isVideoCall() || isVideoUpgradeRequest()) { |
| LogUtil.i("AnswerFragment.setTextResponses", "no-op for video calls"); |
| } else if (textResponses == null) { |
| LogUtil.i("AnswerFragment.setTextResponses", "no text responses, hiding secondary button"); |
| this.textResponses = null; |
| secondaryButton.setVisibility(View.INVISIBLE); |
| } else if (getActivity().isInMultiWindowMode()) { |
| LogUtil.i("AnswerFragment.setTextResponses", "in multiwindow, hiding secondary button"); |
| this.textResponses = null; |
| secondaryButton.setVisibility(View.INVISIBLE); |
| } else { |
| LogUtil.i("AnswerFragment.setTextResponses", "textResponses.size: " + textResponses.size()); |
| this.textResponses = new ArrayList<>(textResponses); |
| secondaryButton.setVisibility(View.VISIBLE); |
| } |
| } |
| |
| private void initSecondaryButton() { |
| secondaryBehavior = |
| isVideoCall() || isVideoUpgradeRequest() |
| ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO |
| : SecondaryBehavior.REJECT_WITH_SMS; |
| secondaryBehavior.applyToView(secondaryButton); |
| |
| secondaryButton.setOnClickListener( |
| new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| performSecondaryButtonAction(); |
| } |
| }); |
| secondaryButton.setClickable(AccessibilityUtil.isAccessibilityEnabled(getContext())); |
| secondaryButton.setFocusable(AccessibilityUtil.isAccessibilityEnabled(getContext())); |
| secondaryButton.setAccessibilityDelegate(accessibilityDelegate); |
| |
| // TODO(wangqi): Remove this when all secondary behavior is migrated to chip button. |
| if (secondaryBehavior.equals(SecondaryBehavior.REJECT_WITH_SMS)) { |
| secondaryButton.setVisibility(View.INVISIBLE); |
| } |
| |
| if (isVideoUpgradeRequest()) { |
| secondaryButton.setVisibility(View.INVISIBLE); |
| } else if (isVideoCall()) { |
| secondaryButton.setVisibility(View.VISIBLE); |
| } |
| |
| answerAndReleaseBehavior = SecondaryBehavior.ANSWER_AND_RELEASE; |
| answerAndReleaseBehavior.applyToView(answerAndReleaseButton); |
| |
| answerAndReleaseButton.setClickable(AccessibilityUtil.isAccessibilityEnabled(getContext())); |
| answerAndReleaseButton.setFocusable(AccessibilityUtil.isAccessibilityEnabled(getContext())); |
| answerAndReleaseButton.setAccessibilityDelegate(accessibilityDelegate); |
| |
| if (allowAnswerAndRelease()) { |
| answerAndReleaseButton.setVisibility(View.VISIBLE); |
| answerScreenDelegate.onAnswerAndReleaseButtonEnabled(); |
| } else { |
| answerAndReleaseButton.setVisibility(View.INVISIBLE); |
| answerScreenDelegate.onAnswerAndReleaseButtonDisabled(); |
| } |
| answerAndReleaseButton.setOnClickListener( |
| new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| performAnswerAndReleaseButtonAction(); |
| } |
| }); |
| } |
| |
| private void addSecondaryActionChip( |
| @DrawableRes int iconRes, @StringRes int textRes, OnClickListener onClickListener) { |
| LinearLayout button = |
| (LinearLayout) |
| getLayoutInflater().inflate(R.layout.secondary_action_chip, chipContainer, false); |
| |
| ImageView icon = button.findViewById(R.id.secondary_action_icon); |
| icon.setImageResource(iconRes); |
| TextView text = button.findViewById(R.id.secondary_action_text); |
| text.setText(textRes); |
| button.setOnClickListener(onClickListener); |
| chipContainer.addView(button); |
| } |
| |
| /** Initialize chip buttons */ |
| private void initChips() { |
| if (allowSpeakEasy()) { |
| Optional<Integer> speakEasyIconOptional = |
| SpeakEasyComponent.get(getContext()).speakEasyIconResource(); |
| Optional<Integer> speakEasyTextOptional = |
| SpeakEasyComponent.get(getContext()).speakEasyTextResource(); |
| if (speakEasyIconOptional.isPresent() && speakEasyTextOptional.isPresent()) { |
| addSecondaryActionChip( |
| speakEasyIconOptional.get(), speakEasyTextOptional.get(), this::performSpeakEasy); |
| } |
| } |
| if (!isVideoCall() && !isVideoUpgradeRequest()) { |
| addSecondaryActionChip( |
| R.drawable.quantum_ic_message_white_24, |
| R.string.call_incoming_reply_with_sms, |
| v -> performSecondaryButtonAction()); |
| } |
| chipContainer.setVisibility(chipContainer.getChildCount() > 0 ? View.VISIBLE : View.GONE); |
| } |
| |
| @Override |
| public boolean allowAnswerAndRelease() { |
| return getArguments().getBoolean(ARG_ALLOW_ANSWER_AND_RELEASE); |
| } |
| |
| @Override |
| public boolean allowSpeakEasy() { |
| return getArguments().getBoolean(ARG_ALLOW_SPEAK_EASY); |
| } |
| |
| private boolean hasCallOnHold() { |
| return getArguments().getBoolean(ARG_HAS_CALL_ON_HOLD); |
| } |
| |
| @Override |
| public boolean hasPendingDialogs() { |
| boolean hasPendingDialogs = |
| textResponsesFragment != null || createCustomSmsDialogFragment != null; |
| LogUtil.i("AnswerFragment.hasPendingDialogs", "" + hasPendingDialogs); |
| return hasPendingDialogs; |
| } |
| |
| @Override |
| public void dismissPendingDialogs() { |
| LogUtil.i("AnswerFragment.dismissPendingDialogs", null); |
| if (textResponsesFragment != null) { |
| textResponsesFragment.dismiss(); |
| textResponsesFragment = null; |
| } |
| |
| if (createCustomSmsDialogFragment != null) { |
| createCustomSmsDialogFragment.dismiss(); |
| createCustomSmsDialogFragment = null; |
| } |
| } |
| |
| @Override |
| public boolean isShowingLocationUi() { |
| Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder); |
| return fragment != null && fragment.isVisible(); |
| } |
| |
| @Override |
| public void showLocationUi(@Nullable Fragment locationUi) { |
| boolean isShowing = isShowingLocationUi(); |
| if (!isShowing && locationUi != null) { |
| // Show the location fragment. |
| getChildFragmentManager() |
| .beginTransaction() |
| .replace(R.id.incall_location_holder, locationUi) |
| .commitAllowingStateLoss(); |
| } else if (isShowing && locationUi == null) { |
| // Hide the location fragment |
| Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder); |
| getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss(); |
| } |
| } |
| |
| @Override |
| public Fragment getAnswerScreenFragment() { |
| return this; |
| } |
| |
| private AnswerMethod getAnswerMethod() { |
| return ((AnswerMethod) |
| getChildFragmentManager().findFragmentById(R.id.answer_method_container)); |
| } |
| |
| @Override |
| public void setPrimary(PrimaryInfo primaryInfo) { |
| LogUtil.i("AnswerFragment.setPrimary", primaryInfo.toString()); |
| this.primaryInfo = primaryInfo; |
| updatePrimaryUI(); |
| updateImportanceBadgeVisibility(); |
| } |
| |
| private void updatePrimaryUI() { |
| if (getView() == null) { |
| return; |
| } |
| contactGridManager.setPrimary(primaryInfo); |
| getAnswerMethod().setShowIncomingWillDisconnect(primaryInfo.answeringDisconnectsOngoingCall()); |
| getAnswerMethod() |
| .setContactPhoto( |
| primaryInfo.photoType() == ContactPhotoType.CONTACT ? primaryInfo.photo() : null); |
| updateDataFragment(); |
| |
| if (primaryInfo.shouldShowLocation()) { |
| // Hide the avatar to make room for location |
| contactGridManager.setAvatarHidden(true); |
| } |
| } |
| |
| private void updateDataFragment() { |
| if (!isAdded()) { |
| return; |
| } |
| LogUtil.enterBlock("AnswerFragment.updateDataFragment"); |
| Fragment current = getChildFragmentManager().findFragmentById(R.id.incall_data_container); |
| Fragment newFragment = null; |
| |
| MultimediaData multimediaData = getSessionData(); |
| if (multimediaData != null |
| && (!TextUtils.isEmpty(multimediaData.getText()) |
| || (multimediaData.getImageUri() != null) |
| || (multimediaData.getLocation() != null && canShowMap()))) { |
| // Need message fragment |
| String subject = multimediaData.getText(); |
| Uri imageUri = multimediaData.getImageUri(); |
| Location location = multimediaData.getLocation(); |
| if (!(current instanceof MultimediaFragment) |
| || !Objects.equals(((MultimediaFragment) current).getSubject(), subject) |
| || !Objects.equals(((MultimediaFragment) current).getImageUri(), imageUri) |
| || !Objects.equals(((MultimediaFragment) current).getLocation(), location)) { |
| LogUtil.i("AnswerFragment.updateDataFragment", "Replacing multimedia fragment"); |
| // Needs replacement |
| newFragment = |
| MultimediaFragment.newInstance( |
| multimediaData, |
| false /* isInteractive */, |
| !primaryInfo.isSpam() /* showAvatar */, |
| primaryInfo.isSpam()); |
| } |
| } else if (shouldShowAvatar()) { |
| // Needs Avatar |
| if (!(current instanceof AvatarFragment)) { |
| LogUtil.i("AnswerFragment.updateDataFragment", "Replacing avatar fragment"); |
| // Needs replacement |
| newFragment = new AvatarFragment(); |
| } |
| } else { |
| // Needs empty |
| if (current != null) { |
| LogUtil.i("AnswerFragment.updateDataFragment", "Removing current fragment"); |
| getChildFragmentManager().beginTransaction().remove(current).commitNow(); |
| } |
| contactGridManager.setAvatarImageView(null, 0, false); |
| } |
| |
| if (newFragment != null) { |
| getChildFragmentManager() |
| .beginTransaction() |
| .replace(R.id.incall_data_container, newFragment) |
| .commitNow(); |
| } |
| } |
| |
| private boolean shouldShowAvatar() { |
| return !isVideoCall() && !isVideoUpgradeRequest(); |
| } |
| |
| private boolean canShowMap() { |
| return MapsComponent.get(getContext()).getMaps().isAvailable(); |
| } |
| |
| @Override |
| public void updateAvatar(AvatarPresenter avatarContainer) { |
| contactGridManager.setAvatarImageView( |
| avatarContainer.getAvatarImageView(), |
| avatarContainer.getAvatarSize(), |
| avatarContainer.shouldShowAnonymousAvatar()); |
| } |
| |
| @Override |
| public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {} |
| |
| @Override |
| public void setCallState(@NonNull PrimaryCallState primaryCallState) { |
| LogUtil.i("AnswerFragment.setCallState", primaryCallState.toString()); |
| this.primaryCallState = primaryCallState; |
| contactGridManager.setCallState(primaryCallState); |
| } |
| |
| @Override |
| public void setEndCallButtonEnabled(boolean enabled, boolean animate) {} |
| |
| @Override |
| public void showManageConferenceCallButton(boolean visible) {} |
| |
| @Override |
| public boolean isManageConferenceVisible() { |
| return false; |
| } |
| |
| @Override |
| public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { |
| contactGridManager.dispatchPopulateAccessibilityEvent(event); |
| // Add prompt of how to accept/decline call with swipe gesture. |
| if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { |
| event |
| .getText() |
| .add(getResources().getString(R.string.a11y_incoming_call_swipe_gesture_prompt)); |
| } |
| } |
| |
| @Override |
| public void showNoteSentToast() {} |
| |
| @Override |
| public void updateInCallScreenColors() {} |
| |
| @Override |
| public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {} |
| |
| @Override |
| public int getAnswerAndDialpadContainerResourceId() { |
| throw Assert.createUnsupportedOperationFailException(); |
| } |
| |
| @Override |
| public Fragment getInCallScreenFragment() { |
| return this; |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| } |
| |
| @Override |
| public View onCreateView( |
| LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
| Trace.beginSection("AnswerFragment.onCreateView"); |
| Bundle arguments = getArguments(); |
| Assert.checkState(arguments.containsKey(ARG_CALL_ID)); |
| Assert.checkState(arguments.containsKey(ARG_IS_RTT_CALL)); |
| Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_CALL)); |
| Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_UPGRADE_REQUEST)); |
| |
| buttonAcceptClicked = false; |
| buttonRejectClicked = false; |
| |
| View view = inflater.inflate(R.layout.fragment_incoming_call, container, false); |
| secondaryButton = (SwipeButtonView) view.findViewById(R.id.incoming_secondary_button); |
| answerAndReleaseButton = (SwipeButtonView) view.findViewById(R.id.incoming_secondary_button2); |
| |
| affordanceHolderLayout = (AffordanceHolderLayout) view.findViewById(R.id.incoming_container); |
| affordanceHolderLayout.setAffordanceCallback(affordanceCallback); |
| |
| chipContainer = view.findViewById(R.id.incall_data_container_chip_container); |
| |
| importanceBadge = view.findViewById(R.id.incall_important_call_badge); |
| importanceBadge |
| .getViewTreeObserver() |
| .addOnGlobalLayoutListener( |
| new OnGlobalLayoutListener() { |
| @Override |
| public void onGlobalLayout() { |
| int leftRightPadding = importanceBadge.getHeight() / 2; |
| importanceBadge.setPadding( |
| leftRightPadding, |
| importanceBadge.getPaddingTop(), |
| leftRightPadding, |
| importanceBadge.getPaddingBottom()); |
| } |
| }); |
| updateImportanceBadgeVisibility(); |
| |
| contactGridManager = new ContactGridManager(view, null, 0, false /* showAnonymousAvatar */); |
| boolean isInMultiWindowMode = getActivity().isInMultiWindowMode(); |
| contactGridManager.onMultiWindowModeChanged(isInMultiWindowMode); |
| |
| Fragment answerMethod = |
| getChildFragmentManager().findFragmentById(R.id.answer_method_container); |
| if (AnswerMethodFactory.needsReplacement(answerMethod)) { |
| getChildFragmentManager() |
| .beginTransaction() |
| .replace( |
| R.id.answer_method_container, AnswerMethodFactory.createAnswerMethod(getActivity())) |
| .commitNow(); |
| } |
| |
| answerScreenDelegate = |
| FragmentUtils.getParentUnsafe(this, AnswerScreenDelegateFactory.class) |
| .newAnswerScreenDelegate(this); |
| |
| initSecondaryButton(); |
| initChips(); |
| |
| int flags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; |
| if (!isInMultiWindowMode |
| && (getActivity().checkSelfPermission(permission.STATUS_BAR) |
| == PackageManager.PERMISSION_GRANTED)) { |
| LogUtil.i("AnswerFragment.onCreateView", "STATUS_BAR permission granted, disabling nav bar"); |
| // These flags will suppress the alert that the activity is in full view mode |
| // during an incoming call on a fresh system/factory reset of the app |
| flags |= STATUS_BAR_DISABLE_BACK | STATUS_BAR_DISABLE_HOME | STATUS_BAR_DISABLE_RECENT; |
| } |
| view.setSystemUiVisibility(flags); |
| if (isVideoCall() || isVideoUpgradeRequest()) { |
| if (VideoUtils.hasCameraPermissionAndShownPrivacyToast(getContext())) { |
| if (isSelfManagedCamera()) { |
| answerVideoCallScreen = new SelfManagedAnswerVideoCallScreen(getCallId(), this, view); |
| } else { |
| answerVideoCallScreen = new AnswerVideoCallScreen(getCallId(), this, view); |
| } |
| } else { |
| view.findViewById(R.id.videocall_video_off).setVisibility(View.VISIBLE); |
| } |
| } |
| |
| Trace.endSection(); |
| return view; |
| } |
| |
| @Override |
| public void onAttach(Context context) { |
| super.onAttach(context); |
| FragmentUtils.checkParent(this, InCallScreenDelegateFactory.class); |
| } |
| |
| @Override |
| public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) { |
| Trace.beginSection("AnswerFragment.onViewCreated"); |
| super.onViewCreated(view, savedInstanceState); |
| createInCallScreenDelegate(); |
| updateUI(); |
| |
| if (savedInstanceState == null || !savedInstanceState.getBoolean(STATE_HAS_ANIMATED_ENTRY)) { |
| ViewUtil.doOnGlobalLayout(view, this::animateEntry); |
| } |
| Trace.endSection(); |
| } |
| |
| @Override |
| public void onResume() { |
| Trace.beginSection("AnswerFragment.onResume"); |
| super.onResume(); |
| LogUtil.i("AnswerFragment.onResume", null); |
| restoreSwipeHintTexts(); |
| inCallScreenDelegate.onInCallScreenResumed(); |
| Trace.endSection(); |
| } |
| |
| @Override |
| public void onStart() { |
| Trace.beginSection("AnswerFragment.onStart"); |
| super.onStart(); |
| LogUtil.i("AnswerFragment.onStart", null); |
| |
| updateUI(); |
| if (answerVideoCallScreen != null) { |
| answerVideoCallScreen.onVideoScreenStart(); |
| } |
| Trace.endSection(); |
| } |
| |
| @Override |
| public void onStop() { |
| Trace.beginSection("AnswerFragment.onStop"); |
| super.onStop(); |
| LogUtil.i("AnswerFragment.onStop", null); |
| |
| handler.removeCallbacks(swipeHintRestoreTimer); |
| if (answerVideoCallScreen != null) { |
| answerVideoCallScreen.onVideoScreenStop(); |
| } |
| Trace.endSection(); |
| } |
| |
| @Override |
| public void onPause() { |
| Trace.beginSection("AnswerFragment.onPause"); |
| super.onPause(); |
| LogUtil.i("AnswerFragment.onPause", null); |
| inCallScreenDelegate.onInCallScreenPaused(); |
| Trace.endSection(); |
| } |
| |
| @Override |
| public void onDestroyView() { |
| LogUtil.i("AnswerFragment.onDestroyView", null); |
| if (answerVideoCallScreen != null) { |
| answerVideoCallScreen = null; |
| } |
| super.onDestroyView(); |
| inCallScreenDelegate.onInCallScreenUnready(); |
| answerScreenDelegate.onAnswerScreenUnready(); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle bundle) { |
| super.onSaveInstanceState(bundle); |
| bundle.putBoolean(STATE_HAS_ANIMATED_ENTRY, hasAnimatedEntry); |
| } |
| |
| private void updateUI() { |
| if (getView() == null) { |
| return; |
| } |
| |
| if (primaryInfo != null) { |
| updatePrimaryUI(); |
| } |
| if (primaryCallState != null) { |
| contactGridManager.setCallState(primaryCallState); |
| } |
| } |
| |
| @Override |
| public boolean isRttCall() { |
| return getArguments().getBoolean(ARG_IS_RTT_CALL); |
| } |
| |
| @Override |
| public boolean isVideoCall() { |
| return getArguments().getBoolean(ARG_IS_VIDEO_CALL); |
| } |
| |
| public boolean isSelfManagedCamera() { |
| return getArguments().getBoolean(ARG_IS_SELF_MANAGED_CAMERA); |
| } |
| |
| @Override |
| public void onAnswerProgressUpdate(@FloatRange(from = -1f, to = 1f) float answerProgress) { |
| // Fade and scale contact name and video call text |
| float startDelay = .25f; |
| // Header progress is zero over positiveAdjustedProgress = [0, startDelay], |
| // linearly increases over (startDelay, 1] until reaching 1 when positiveAdjustedProgress = 1 |
| float headerProgress = Math.max(0, (Math.abs(answerProgress) - 1) / (1 - startDelay) + 1); |
| fadeToward(contactGridManager.getContainerView(), 1 - headerProgress); |
| scaleToward(contactGridManager.getContainerView(), MathUtil.lerp(1f, .75f, headerProgress)); |
| |
| if (Math.abs(answerProgress) >= .0001) { |
| affordanceHolderLayout.animateHideLeftRightIcon(); |
| handler.removeCallbacks(swipeHintRestoreTimer); |
| restoreSwipeHintTexts(); |
| } |
| } |
| |
| @Override |
| public void answerFromMethod() { |
| acceptCallByUser(false /* answerVideoAsAudio */); |
| } |
| |
| @Override |
| public void rejectFromMethod() { |
| rejectCall(); |
| } |
| |
| @Override |
| public void resetAnswerProgress() { |
| affordanceHolderLayout.reset(true); |
| } |
| |
| private void animateEntry(@NonNull View rootView) { |
| if (!isAdded()) { |
| LogUtil.i( |
| "AnswerFragment.animateEntry", |
| "Not currently added to Activity. Will not start entry animation."); |
| return; |
| } |
| contactGridManager.getContainerView().setAlpha(0f); |
| Animator alpha = |
| ObjectAnimator.ofFloat(contactGridManager.getContainerView(), View.ALPHA, 0, 1); |
| Animator topRow = createTranslation(rootView.findViewById(R.id.contactgrid_top_row)); |
| Animator contactName = createTranslation(rootView.findViewById(R.id.contactgrid_contact_name)); |
| Animator bottomRow = createTranslation(rootView.findViewById(R.id.contactgrid_bottom_row)); |
| Animator important = createTranslation(importanceBadge); |
| Animator dataContainer = createTranslation(rootView.findViewById(R.id.incall_data_container)); |
| |
| AnimatorSet animatorSet = new AnimatorSet(); |
| AnimatorSet.Builder builder = animatorSet.play(alpha); |
| builder.with(topRow).with(contactName).with(bottomRow).with(important).with(dataContainer); |
| if (isShowingLocationUi()) { |
| builder.with(createTranslation(rootView.findViewById(R.id.incall_location_holder))); |
| } |
| animatorSet.setDuration( |
| rootView.getResources().getInteger(R.integer.answer_animate_entry_millis)); |
| animatorSet.addListener( |
| new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| hasAnimatedEntry = true; |
| } |
| }); |
| animatorSet.start(); |
| } |
| |
| private ObjectAnimator createTranslation(View view) { |
| float translationY = view.getTop() * 0.5f; |
| ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, translationY, 0); |
| animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); |
| return animator; |
| } |
| |
| private void acceptCallByUser(boolean answerVideoAsAudio) { |
| LogUtil.i("AnswerFragment.acceptCallByUser", answerVideoAsAudio ? " answerVideoAsAudio" : ""); |
| if (!buttonAcceptClicked) { |
| answerScreenDelegate.onAnswer(answerVideoAsAudio); |
| buttonAcceptClicked = true; |
| } |
| } |
| |
| private void rejectCall() { |
| LogUtil.i("AnswerFragment.rejectCall", null); |
| if (!buttonRejectClicked) { |
| Context context = getContext(); |
| if (context == null) { |
| LogUtil.w( |
| "AnswerFragment.rejectCall", |
| "Null context when rejecting call. Logger call was skipped"); |
| } else { |
| Logger.get(context).logImpression(Type.REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN); |
| } |
| buttonRejectClicked = true; |
| answerScreenDelegate.onReject(); |
| } |
| } |
| |
| private void restoreSwipeHintTexts() { |
| if (getAnswerMethod() != null) { |
| if (allowAnswerAndRelease()) { |
| if (hasCallOnHold()) { |
| getAnswerMethod() |
| .setHintText(getText(R.string.call_incoming_default_label_answer_and_release_third)); |
| } else if (primaryCallState.supportsCallOnHold()) { |
| getAnswerMethod() |
| .setHintText(getText(R.string.call_incoming_default_label_answer_and_release_second)); |
| } |
| } else { |
| getAnswerMethod().setHintText(null); |
| } |
| } |
| } |
| |
| private void showMessageMenu() { |
| LogUtil.i("AnswerFragment.showMessageMenu", "Show sms menu."); |
| if (getContext() == null || isDetached() || getChildFragmentManager().isDestroyed()) { |
| return; |
| } |
| |
| textResponsesFragment = SmsBottomSheetFragment.newInstance(textResponses); |
| textResponsesFragment.show(getChildFragmentManager(), null); |
| secondaryButton |
| .animate() |
| .alpha(0) |
| .withEndAction( |
| new Runnable() { |
| @Override |
| public void run() { |
| affordanceHolderLayout.reset(false); |
| secondaryButton.animate().alpha(1); |
| } |
| }); |
| |
| TelecomUtil.silenceRinger(getContext()); |
| } |
| |
| @Override |
| public InCallUiLock acquireInCallUiLock(String tag) { |
| return answerScreenDelegate.acquireInCallUiLock(tag); |
| } |
| |
| @Override |
| public void smsSelected(@Nullable CharSequence text) { |
| LogUtil.i("AnswerFragment.smsSelected", null); |
| textResponsesFragment = null; |
| |
| if (text == null) { |
| createCustomSmsDialogFragment = CreateCustomSmsDialogFragment.newInstance(); |
| createCustomSmsDialogFragment.show(getChildFragmentManager(), null); |
| return; |
| } |
| |
| if (primaryCallState != null && canRejectCallWithSms()) { |
| rejectCall(); |
| answerScreenDelegate.onRejectCallWithMessage(text.toString()); |
| } |
| } |
| |
| @Override |
| public void smsDismissed() { |
| LogUtil.i("AnswerFragment.smsDismissed", null); |
| textResponsesFragment = null; |
| } |
| |
| @Override |
| public void customSmsCreated(@NonNull CharSequence text) { |
| LogUtil.i("AnswerFragment.customSmsCreated", null); |
| createCustomSmsDialogFragment = null; |
| if (primaryCallState != null && canRejectCallWithSms()) { |
| rejectCall(); |
| answerScreenDelegate.onRejectCallWithMessage(text.toString()); |
| } |
| } |
| |
| @Override |
| public void customSmsDismissed() { |
| LogUtil.i("AnswerFragment.customSmsDismissed", null); |
| createCustomSmsDialogFragment = null; |
| } |
| |
| private boolean canRejectCallWithSms() { |
| return primaryCallState != null |
| && !(primaryCallState.state() == DialerCallState.DISCONNECTED |
| || primaryCallState.state() == DialerCallState.DISCONNECTING |
| || primaryCallState.state() == DialerCallState.IDLE); |
| } |
| |
| private void createInCallScreenDelegate() { |
| inCallScreenDelegate = |
| FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class) |
| .newInCallScreenDelegate(); |
| Assert.isNotNull(inCallScreenDelegate); |
| inCallScreenDelegate.onInCallScreenDelegateInit(this); |
| inCallScreenDelegate.onInCallScreenReady(); |
| } |
| |
| private void updateImportanceBadgeVisibility() { |
| if (!isAdded() || getView() == null) { |
| return; |
| } |
| |
| if (!getResources().getBoolean(R.bool.answer_important_call_allowed) || primaryInfo.isSpam()) { |
| importanceBadge.setVisibility(View.GONE); |
| return; |
| } |
| |
| MultimediaData multimediaData = getSessionData(); |
| boolean showImportant = multimediaData != null && multimediaData.isImportant(); |
| TransitionManager.beginDelayedTransition((ViewGroup) importanceBadge.getParent()); |
| // TODO (keyboardr): Change this back to being View.INVISIBLE once mocks are available to |
| // properly handle smaller screens |
| importanceBadge.setVisibility(showImportant ? View.VISIBLE : View.GONE); |
| } |
| |
| @Nullable |
| private MultimediaData getSessionData() { |
| if (primaryInfo == null) { |
| return null; |
| } |
| if (isVideoUpgradeRequest()) { |
| return null; |
| } |
| return primaryInfo.multimediaData(); |
| } |
| |
| /** Shows the Avatar image if available. */ |
| public static class AvatarFragment extends Fragment implements AvatarPresenter { |
| |
| private ImageView avatarImageView; |
| |
| @Nullable |
| @Override |
| public View onCreateView( |
| LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) { |
| return layoutInflater.inflate(R.layout.fragment_avatar, viewGroup, false); |
| } |
| |
| @Override |
| public void onViewCreated(View view, @Nullable Bundle bundle) { |
| super.onViewCreated(view, bundle); |
| avatarImageView = ((ImageView) view.findViewById(R.id.contactgrid_avatar)); |
| FragmentUtils.getParentUnsafe(this, Holder.class).updateAvatar(this); |
| } |
| |
| @NonNull |
| @Override |
| public ImageView getAvatarImageView() { |
| return avatarImageView; |
| } |
| |
| @Override |
| public int getAvatarSize() { |
| return getResources().getDimensionPixelSize(R.dimen.answer_avatar_size); |
| } |
| |
| @Override |
| public boolean shouldShowAnonymousAvatar() { |
| return false; |
| } |
| } |
| } |