diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 8986d874af..5bfd8844cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -148,6 +148,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan private FullscreenHelper fullscreenHelper; private WebRtcCallView callScreen; private TooltipPopup videoTooltip; + private TooltipPopup switchCameraTooltip; private WebRtcCallViewModel viewModel; private boolean enableVideoIfAvailable; private boolean hasWarnedAboutBluetooth; @@ -549,6 +550,20 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan } } else if (event instanceof WebRtcCallViewModel.Event.ShowWifiToCellularPopup) { wifiToCellularPopupWindow.show(); + } else if (event instanceof WebRtcCallViewModel.Event.ShowSwitchCameraTooltip) { + if (switchCameraTooltip == null) { + switchCameraTooltip = TooltipPopup.forTarget(callScreen.getSwitchCameraTooltipTarget()) + .setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine)) + .setTextColor(ContextCompat.getColor(this, R.color.core_white)) + .setText(R.string.WebRtcCallActivity__flip_camera_tooltip) + .setOnDismissListener(() -> viewModel.onDismissedSwitchCameraTooltip()) + .show(TooltipPopup.POSITION_ABOVE); + } + } else if (event instanceof WebRtcCallViewModel.Event.DismissSwitchCameraTooltip) { + if (switchCameraTooltip != null) { + switchCameraTooltip.dismiss(); + switchCameraTooltip = null; + } } else { throw new IllegalArgumentException("Unknown event: " + event); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TooltipPopup.java b/app/src/main/java/org/thoughtcrime/securesms/components/TooltipPopup.java index 40e12f056d..82622774f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/TooltipPopup.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/TooltipPopup.java @@ -171,21 +171,23 @@ public class TooltipPopup extends PopupWindow { ShapeAppearanceModel.Builder shapeAppearanceModel = ShapeAppearanceModel.builder() .setAllCornerSizes(DimensionUnit.DP.toPixels(18)); - // If the arrow is within the last 20dp of the right hand side, use RIGHT and set corner to 9dp - onLayout(() -> { - if (arrow.getX() > getContentView().getWidth() / 2f) { - arrow.setImageResource(R.drawable.ic_tooltip_arrow_up_right); - } + if (position == POSITION_BELOW) { + // If the arrow is within the last 20dp of the right hand side, use RIGHT and set corner to 9dp + onLayout(() -> { + if (arrow.getX() > getContentView().getWidth() / 2f) { + arrow.setImageResource(R.drawable.ic_tooltip_arrow_up_right); + } - float arrowEnd = arrow.getX() + arrow.getRight(); - if (arrowEnd > getContentView().getRight() - DimensionUnit.DP.toPixels(20)) { - shapeableBubbleBackground.setShapeAppearanceModel(shapeAppearanceModel.setTopRightCornerSize(DimensionUnit.DP.toPixels(9f)).build()); - bubble.setBackground(shapeableBubbleBackground); - } else if (arrowEnd < DimensionUnit.DP.toPixels(20)) { - shapeableBubbleBackground.setShapeAppearanceModel(shapeAppearanceModel.setTopLeftCornerSize(DimensionUnit.DP.toPixels(9f)).build()); - bubble.setBackground(shapeableBubbleBackground); - } - }); + float arrowEnd = arrow.getX() + arrow.getRight(); + if (arrowEnd > getContentView().getRight() - DimensionUnit.DP.toPixels(20)) { + shapeableBubbleBackground.setShapeAppearanceModel(shapeAppearanceModel.setTopRightCornerSize(DimensionUnit.DP.toPixels(9f)).build()); + bubble.setBackground(shapeableBubbleBackground); + } else if (arrowEnd < DimensionUnit.DP.toPixels(20)) { + shapeableBubbleBackground.setShapeAppearanceModel(shapeAppearanceModel.setTopLeftCornerSize(DimensionUnit.DP.toPixels(9f)).build()); + bubble.setBackground(shapeableBubbleBackground); + } + }); + } try { showAsDropDown(anchor, xoffset, yoffset); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java index 7d4d7dbcec..af063c6d99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java @@ -74,6 +74,8 @@ public class CallParticipantView extends ConstraintLayout { private EmojiTextView infoMessage; private Button infoMoreInfo; private AppCompatImageView infoIcon; + private View switchCameraIconFrame; + private View switchCameraIcon; public CallParticipantView(@NonNull Context context) { super(context); @@ -92,18 +94,20 @@ public class CallParticipantView extends ConstraintLayout { protected void onFinishInflate() { super.onFinishInflate(); - backgroundAvatar = findViewById(R.id.call_participant_background_avatar); - avatar = findViewById(R.id.call_participant_item_avatar); - pipAvatar = findViewById(R.id.call_participant_item_pip_avatar); - rendererFrame = findViewById(R.id.call_participant_renderer_frame); - renderer = findViewById(R.id.call_participant_renderer); - audioIndicator = findViewById(R.id.call_participant_audio_indicator); - infoOverlay = findViewById(R.id.call_participant_info_overlay); - infoIcon = findViewById(R.id.call_participant_info_icon); - infoMessage = findViewById(R.id.call_participant_info_message); - infoMoreInfo = findViewById(R.id.call_participant_info_more_info); - badge = findViewById(R.id.call_participant_item_badge); - pipBadge = findViewById(R.id.call_participant_item_pip_badge); + backgroundAvatar = findViewById(R.id.call_participant_background_avatar); + avatar = findViewById(R.id.call_participant_item_avatar); + pipAvatar = findViewById(R.id.call_participant_item_pip_avatar); + rendererFrame = findViewById(R.id.call_participant_renderer_frame); + renderer = findViewById(R.id.call_participant_renderer); + audioIndicator = findViewById(R.id.call_participant_audio_indicator); + infoOverlay = findViewById(R.id.call_participant_info_overlay); + infoIcon = findViewById(R.id.call_participant_info_icon); + infoMessage = findViewById(R.id.call_participant_info_message); + infoMoreInfo = findViewById(R.id.call_participant_info_more_info); + badge = findViewById(R.id.call_participant_item_badge); + pipBadge = findViewById(R.id.call_participant_item_pip_badge); + switchCameraIconFrame = findViewById(R.id.call_participant_switch_camera); + switchCameraIcon = findViewById(R.id.call_participant_switch_camera_icon); avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER); useLargeAvatar(); @@ -249,6 +253,27 @@ public class CallParticipantView extends ConstraintLayout { ConstraintSet.BOTTOM, ViewUtil.dpToPx(6) ); + + constraints.setVisibility(R.id.call_participant_switch_camera, View.VISIBLE); + constraints.setMargin( + R.id.call_participant_switch_camera, + ConstraintSet.END, + ViewUtil.dpToPx(6) + ); + constraints.setMargin( + R.id.call_participant_switch_camera, + ConstraintSet.BOTTOM, + ViewUtil.dpToPx(6) + ); + constraints.constrainWidth(R.id.call_participant_switch_camera, ViewUtil.dpToPx(28)); + constraints.constrainHeight(R.id.call_participant_switch_camera, ViewUtil.dpToPx(28)); + + ViewGroup.LayoutParams params = switchCameraIcon.getLayoutParams(); + params.width = params.height = ViewUtil.dpToPx(16); + switchCameraIcon.setLayoutParams(params); + + switchCameraIconFrame.setClickable(false); + switchCameraIconFrame.setEnabled(false); } case EXPANDED_SELF_PIP -> { constraints.connect( @@ -267,6 +292,27 @@ public class CallParticipantView extends ConstraintLayout { ConstraintSet.BOTTOM, ViewUtil.dpToPx(8) ); + + constraints.setVisibility(R.id.call_participant_switch_camera, View.VISIBLE); + constraints.setMargin( + R.id.call_participant_switch_camera, + ConstraintSet.END, + ViewUtil.dpToPx(8) + ); + constraints.setMargin( + R.id.call_participant_switch_camera, + ConstraintSet.BOTTOM, + ViewUtil.dpToPx(8) + ); + constraints.constrainWidth(R.id.call_participant_switch_camera, ViewUtil.dpToPx(48)); + constraints.constrainHeight(R.id.call_participant_switch_camera, ViewUtil.dpToPx(48)); + + ViewGroup.LayoutParams params = switchCameraIcon.getLayoutParams(); + params.width = params.height = ViewUtil.dpToPx(24); + switchCameraIcon.setLayoutParams(params); + + switchCameraIconFrame.setClickable(true); + switchCameraIconFrame.setEnabled(true); } case MINI_SELF_PIP -> { constraints.connect( @@ -288,6 +334,7 @@ public class CallParticipantView extends ConstraintLayout { ConstraintSet.BOTTOM, ViewUtil.dpToPx(6) ); + constraints.setVisibility(R.id.call_participant_switch_camera, View.GONE); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java index 509ee41c68..10c0913468 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java @@ -278,6 +278,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { }); cameraDirectionToggle.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged)); + smallLocalRender.findViewById(R.id.call_participant_switch_camera).setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged)); overflow.setOnClickListener(v -> { runIfNonNull(controlsListener, ControlsListener::onOverflowClicked); @@ -794,6 +795,10 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { return videoToggle; } + public @NonNull View getSwitchCameraTooltipTarget() { + return smallLocalRenderFrame; + } + public void showSpeakerViewHint() { groupCallSpeakerHint.get().setVisibility(View.VISIBLE); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java index d377c69ca5..53d865e7c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java @@ -81,17 +81,18 @@ public class WebRtcCallViewModel extends ViewModel { private final Runnable elapsedTimeRunnable = this::handleTick; private final Runnable stopOutgoingRingingMode = this::stopOutgoingRingingMode; - private boolean canDisplayTooltipIfNeeded = true; - private boolean canDisplayPopupIfNeeded = true; - private boolean hasEnabledLocalVideo = false; - private boolean wasInOutgoingRingingMode = false; - private long callConnectedTime = -1; - private boolean answerWithVideoAvailable = false; - private boolean canEnterPipMode = false; - private List previousParticipantsList = Collections.emptyList(); - private boolean callStarting = false; - private boolean switchOnFirstScreenShare = true; - private boolean showScreenShareTip = true; + private boolean canDisplayTooltipIfNeeded = true; + private boolean canDisplaySwitchCameraTooltipIfNeeded = true; + private boolean canDisplayPopupIfNeeded = true; + private boolean hasEnabledLocalVideo = false; + private boolean wasInOutgoingRingingMode = false; + private long callConnectedTime = -1; + private boolean answerWithVideoAvailable = false; + private boolean canEnterPipMode = false; + private List previousParticipantsList = Collections.emptyList(); + private boolean callStarting = false; + private boolean switchOnFirstScreenShare = true; + private boolean showScreenShareTip = true; private final WebRtcCallRepository repository = new WebRtcCallRepository(ApplicationDependencies.getApplication()); @@ -261,6 +262,11 @@ public class WebRtcCallViewModel extends ViewModel { canDisplayTooltipIfNeeded = false; } + public void onDismissedSwitchCameraTooltip() { + canDisplaySwitchCameraTooltipIfNeeded = false; + SignalStore.tooltips().markCallingSwitchCameraTooltipSeen(); + } + @MainThread public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) { canEnterPipMode = !webRtcViewModel.getState().isPreJoinOrNetworkUnavailable(); @@ -342,6 +348,16 @@ public class WebRtcCallViewModel extends ViewModel { } else if (!webRtcViewModel.isCellularConnection()) { canDisplayPopupIfNeeded = true; } + + if (SignalStore.tooltips().showCallingSwitchCameraTooltip() && + canDisplaySwitchCameraTooltipIfNeeded && + hasEnabledLocalVideo && + webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && + !newState.getAllRemoteParticipants().isEmpty() + ) { + canDisplaySwitchCameraTooltipIfNeeded = false; + events.setValue(new Event.ShowSwitchCameraTooltip()); + } } @MainThread @@ -537,6 +553,12 @@ public class WebRtcCallViewModel extends ViewModel { public static class ShowWifiToCellularPopup extends Event { } + public static class ShowSwitchCameraTooltip extends Event { + } + + public static class DismissSwitchCameraTooltip extends Event { + } + public static class StartCall extends Event { private final boolean isVideoCall; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java index 497f3ff70e..4490963072 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java @@ -157,7 +157,7 @@ public final class WebRtcControls { } public boolean displayOverflow() { - return FeatureFlags.groupCallReactions() && isAtLeastOutgoing(); + return FeatureFlags.groupCallReactions() && isAtLeastOutgoing() && hasAtLeastOneRemote && isGroupCall(); } public boolean displayMuteAudio() { @@ -173,7 +173,7 @@ public final class WebRtcControls { } public boolean displayCameraToggle() { - return (isPreJoin() || isAtLeastOutgoing()) && isLocalVideoEnabled && isMoreThanOneCameraAvailable; + return (isPreJoin() || (isAtLeastOutgoing() && !hasAtLeastOneRemote)) && isLocalVideoEnabled && isMoreThanOneCameraAvailable; } public boolean displayRemoteVideoRecycler() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/TooltipValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/TooltipValues.java index fbc0e86b31..5a255bb378 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/TooltipValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/TooltipValues.java @@ -15,7 +15,7 @@ public class TooltipValues extends SignalStoreValues { private static final String MULTI_FORWARD_DIALOG = "tooltip.multi.forward.dialog"; private static final String BUBBLE_OPT_OUT = "tooltip.bubble.opt.out"; private static final String PROFILE_SETTINGS_QR_CODE = "tooltip.profile_settings_qr_code"; - + private static final String CALLING_SWITCH_CAMERA = "tooltip.calling.switch_camera"; TooltipValues(@NonNull KeyValueStore store) { super(store); @@ -82,4 +82,12 @@ public class TooltipValues extends SignalStoreValues { public void markProfileSettingsQrCodeTooltipSeen() { putBoolean(PROFILE_SETTINGS_QR_CODE, false); } + + public boolean showCallingSwitchCameraTooltip() { + return getBoolean(CALLING_SWITCH_CAMERA, true); + } + + public void markCallingSwitchCameraTooltipSeen() { + putBoolean(CALLING_SWITCH_CAMERA, false); + } } diff --git a/app/src/main/res/layout/call_participant_item.xml b/app/src/main/res/layout/call_participant_item.xml index d3007a370b..521189a4cf 100644 --- a/app/src/main/res/layout/call_participant_item.xml +++ b/app/src/main/res/layout/call_participant_item.xml @@ -1,12 +1,12 @@ + tools:layout_width="match_parent" + tools:viewBindingIgnore="true"> + + + + + + Answered on a linked device. Declined on a linked device. Busy on a linked device. + + Flip Camera has been moved here, tap your video to try it out Someone has joined this call with a safety number that has changed.