hide button in video call after timeout

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java | 131 ++
src/main/res/layout/activity_rtp_session.xml                    |   7 
src/main/res/values/strings.xml                                 |   1 
3 files changed, 116 insertions(+), 23 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java ๐Ÿ”—

@@ -32,6 +32,7 @@ import androidx.annotation.RequiresApi;
 import androidx.annotation.StringRes;
 import androidx.databinding.DataBindingUtil;
 
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
@@ -88,7 +89,8 @@ public class RtpSessionActivity extends XmppActivity
     public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call";
     public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call";
 
-    private static final int CALL_DURATION_UPDATE_INTERVAL = 333;
+    private static final int CALL_DURATION_UPDATE_INTERVAL = 250;
+    private static final int BUTTON_VISIBILITY_TIMEOUT = 10_000;
 
     private static final List<RtpEndUserState> END_CARD =
             Arrays.asList(
@@ -143,6 +145,8 @@ public class RtpSessionActivity extends XmppActivity
                     mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
                 }
             };
+    private boolean buttonsHiddenAfterTimeout = false;
+    private final Runnable mVisibilityToggleExecutor = this::updateButtonInVideoCallVisibility;
 
     public static Set<Media> actionToMedia(final String action) {
         if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
@@ -179,10 +183,16 @@ public class RtpSessionActivity extends XmppActivity
                                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                                 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
+        this.binding.remoteVideo.setOnClickListener(this::onVideoScreenClick);
+        this.binding.localVideo.setOnClickListener(this::onVideoScreenClick);
         setSupportActionBar(binding.toolbar);
         Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
     }
 
+    private void onVideoScreenClick(final View view) {
+        resetVisibilityExecutorShowButtons();
+    }
+
     @Override
     public boolean onCreateOptionsMenu(final Menu menu) {
         getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
@@ -605,12 +615,20 @@ public class RtpSessionActivity extends XmppActivity
     public void onStart() {
         super.onStart();
         mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
+        mHandler.postDelayed(mVisibilityToggleExecutor, BUTTON_VISIBILITY_TIMEOUT);
         this.binding.remoteVideo.setOnAspectRatioChanged(this);
     }
 
+    @Override
+    public void onResume() {
+        super.onResume();
+        resetVisibilityExecutorShowButtons();
+    }
+
     @Override
     public void onStop() {
         mHandler.removeCallbacks(mTickExecutor);
+        mHandler.removeCallbacks(mVisibilityToggleExecutor);
         binding.remoteVideo.release();
         binding.remoteVideo.setOnAspectRatioChanged(null);
         binding.localVideo.release();
@@ -732,6 +750,17 @@ public class RtpSessionActivity extends XmppActivity
         }
     }
 
+    private boolean isInConnectedVideoCall() {
+        final JingleRtpConnection rtpConnection;
+        try {
+            rtpConnection = requireRtpConnection();
+        } catch (final IllegalStateException e) {
+            return false;
+        }
+        return rtpConnection.getMedia().contains(Media.VIDEO)
+                && rtpConnection.getEndUserState() == RtpEndUserState.CONNECTED;
+    }
+
     private boolean initializeActivityWithRunningRtpSession(
             final Account account, Jid with, String sessionId) {
         final WeakReference<JingleRtpConnection> reference =
@@ -834,7 +863,7 @@ public class RtpSessionActivity extends XmppActivity
             final ContentAddition contentAddition) {
         switch (state) {
             case INCOMING_CALL -> {
-                Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
+                Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty");
                 if (media.contains(Media.VIDEO)) {
                     setTitle(R.string.rtp_state_incoming_video_call);
                 } else {
@@ -925,7 +954,9 @@ public class RtpSessionActivity extends XmppActivity
             final RtpEndUserState state,
             final Set<Media> media,
             final ContentAddition contentAddition) {
-        if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
+        if (state == RtpEndUserState.ENDING_CALL
+                || isPictureInPicture()
+                || this.buttonsHiddenAfterTimeout) {
             this.binding.rejectCall.setVisibility(View.INVISIBLE);
             this.binding.endCall.setVisibility(View.INVISIBLE);
             this.binding.acceptCall.setVisibility(View.INVISIBLE);
@@ -982,12 +1013,17 @@ public class RtpSessionActivity extends XmppActivity
             this.binding.endCall.setContentDescription(getString(R.string.hang_up));
             this.binding.endCall.setOnClickListener(this::endCall);
             this.binding.endCall.setImageResource(R.drawable.ic_call_end_24dp);
-            this.binding.endCall.setVisibility(View.VISIBLE);
+            setVisibleAndShow(this.binding.endCall);
             this.binding.acceptCall.setVisibility(View.INVISIBLE);
         }
         updateInCallButtonConfiguration(state, media);
     }
 
+    private static void setVisibleAndShow(final FloatingActionButton button) {
+        button.show();
+        button.setVisibility(View.VISIBLE);
+    }
+
     private boolean isPictureInPicture() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             return isInPictureInPictureMode();
@@ -1004,7 +1040,8 @@ public class RtpSessionActivity extends XmppActivity
     @SuppressLint("RestrictedApi")
     private void updateInCallButtonConfiguration(
             final RtpEndUserState state, final Set<Media> media) {
-        if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) {
+        final var showButtons = !isPictureInPicture() && !buttonsHiddenAfterTimeout;
+        if (STATES_CONSIDERED_CONNECTED.contains(state) && showButtons) {
             Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty");
             if (media.contains(Media.VIDEO)) {
                 final JingleRtpConnection rtpConnection = requireRtpConnection();
@@ -1024,7 +1061,7 @@ public class RtpSessionActivity extends XmppActivity
                 this.binding.inCallActionLeft.setVisibility(View.GONE);
             }
         } else if (STATES_SHOWING_SPEAKER_CONFIGURATION.contains(state)
-                && !isPictureInPicture()
+                && showButtons
                 && Media.audioOnly(media)) {
             final CallIntegration callIntegration;
             try {
@@ -1089,17 +1126,17 @@ public class RtpSessionActivity extends XmppActivity
                 this.binding.inCallActionRight.setClickable(false);
             }
         }
-        this.binding.inCallActionRight.setVisibility(View.VISIBLE);
+        setVisibleAndShow(this.binding.inCallActionRight);
     }
 
     @SuppressLint("RestrictedApi")
     private void updateInCallButtonConfigurationVideo(
             final boolean videoEnabled, final boolean isCameraSwitchable) {
-        this.binding.inCallActionRight.setVisibility(View.VISIBLE);
+        setVisibleAndShow(this.binding.inCallActionRight);
         if (isCameraSwitchable) {
             this.binding.inCallActionFarRight.setImageResource(
                     R.drawable.ic_flip_camera_android_24dp);
-            this.binding.inCallActionFarRight.setVisibility(View.VISIBLE);
+            setVisibleAndShow(this.binding.inCallActionFarRight);
             this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera);
             this.binding.inCallActionFarRight.setContentDescription(
                     getString(R.string.flip_camera));
@@ -1120,6 +1157,7 @@ public class RtpSessionActivity extends XmppActivity
     }
 
     private void switchCamera(final View view) {
+        resetVisibilityToggleExecutor();
         Futures.addCallback(
                 requireRtpConnection().switchCamera(),
                 new FutureCallback<>() {
@@ -1145,6 +1183,7 @@ public class RtpSessionActivity extends XmppActivity
     }
 
     private void enableVideo(final View view) {
+        resetVisibilityToggleExecutor();
         try {
             requireRtpConnection().setVideoEnabled(true);
         } catch (final IllegalStateException e) {
@@ -1155,6 +1194,7 @@ public class RtpSessionActivity extends XmppActivity
     }
 
     private void disableVideo(final View view) {
+        resetVisibilityToggleExecutor();
         final JingleRtpConnection rtpConnection = requireRtpConnection();
         final ContentAddition pending = rtpConnection.getPendingContentAddition();
         if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) {
@@ -1179,7 +1219,7 @@ public class RtpSessionActivity extends XmppActivity
             this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_24dp);
             this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone);
         }
-        this.binding.inCallActionLeft.setVisibility(View.VISIBLE);
+        setVisibleAndShow(this.binding.inCallActionLeft);
     }
 
     private void updateCallDuration() {
@@ -1198,6 +1238,47 @@ public class RtpSessionActivity extends XmppActivity
         }
     }
 
+    private void resetVisibilityToggleExecutor() {
+        mHandler.removeCallbacks(this.mVisibilityToggleExecutor);
+        mHandler.postDelayed(this.mVisibilityToggleExecutor, BUTTON_VISIBILITY_TIMEOUT);
+    }
+
+    private void updateButtonInVideoCallVisibility() {
+        if (isInConnectedVideoCall()) {
+            if (isPictureInPicture()) {
+                return;
+            }
+            Log.d(Config.LOGTAG, "hiding in-call buttons after timeout was reached");
+            hideInCallButtons();
+        }
+    }
+
+    private void hideInCallButtons() {
+        binding.inCallActionLeft.hide();
+        binding.endCall.hide();
+        binding.inCallActionRight.hide();
+        binding.inCallActionFarRight.hide();
+    }
+
+    private void showInCallButtons() {
+        this.buttonsHiddenAfterTimeout = false;
+        final JingleRtpConnection rtpConnection;
+        try {
+            rtpConnection = requireRtpConnection();
+        } catch (final IllegalStateException e) {
+            return;
+        }
+        updateButtonConfiguration(
+                rtpConnection.getEndUserState(),
+                rtpConnection.getMedia(),
+                rtpConnection.getPendingContentAddition());
+    }
+
+    private void resetVisibilityExecutorShowButtons() {
+        resetVisibilityToggleExecutor();
+        showInCallButtons();
+    }
+
     private void updateVideoViews(final RtpEndUserState state) {
         if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
             binding.localVideo.setVisibility(View.GONE);
@@ -1292,17 +1373,23 @@ public class RtpSessionActivity extends XmppActivity
         return connection.getRemoteVideoTrack();
     }
 
-    private void disableMicrophone(View view) {
-        final JingleRtpConnection rtpConnection = requireRtpConnection();
-        if (rtpConnection.setMicrophoneEnabled(false)) {
-            updateInCallButtonConfiguration();
-        }
+    private void disableMicrophone(final View view) {
+        setMicrophoneEnabled(false);
     }
 
-    private void enableMicrophone(View view) {
-        final JingleRtpConnection rtpConnection = requireRtpConnection();
-        if (rtpConnection.setMicrophoneEnabled(true)) {
-            updateInCallButtonConfiguration();
+    private void enableMicrophone(final View view) {
+        setMicrophoneEnabled(true);
+    }
+
+    private void setMicrophoneEnabled(final boolean enabled) {
+        resetVisibilityExecutorShowButtons();
+        try {
+            final JingleRtpConnection rtpConnection = requireRtpConnection();
+            if (rtpConnection.setMicrophoneEnabled(enabled)) {
+                updateInCallButtonConfiguration();
+            }
+        } catch (final IllegalStateException e) {
+            Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
         }
     }
 
@@ -1311,7 +1398,7 @@ public class RtpSessionActivity extends XmppActivity
             requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE);
             acquireProximityWakeLock();
         } catch (final IllegalStateException e) {
-            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
+            Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
         }
     }
 
@@ -1320,7 +1407,7 @@ public class RtpSessionActivity extends XmppActivity
             requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE);
             releaseProximityWakeLock();
         } catch (final IllegalStateException e) {
-            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
+            Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
         }
     }
 
@@ -1410,6 +1497,7 @@ public class RtpSessionActivity extends XmppActivity
                     () -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
         }
         if (with.isBareJid()) {
+            // TODO check for ENDED
             updateRtpSessionProposalState(account, with, state);
             return;
         }
@@ -1433,6 +1521,7 @@ public class RtpSessionActivity extends XmppActivity
                 finish();
                 return;
             }
+            resetVisibilityToggleExecutor();
             runOnUiThread(
                     () -> {
                         updateStateDisplay(state, media, contentAddition);

src/main/res/layout/activity_rtp_session.xml ๐Ÿ”—

@@ -112,7 +112,8 @@
             <eu.siacs.conversations.ui.widget.SurfaceViewRenderer
                 android:id="@+id/remote_video"
                 android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
+                android:layout_height="wrap_content"
+                android:soundEffectsEnabled="false" />
         </LinearLayout>
 
         <eu.siacs.conversations.ui.widget.SurfaceViewRenderer
@@ -123,6 +124,7 @@
             android:layout_alignParentEnd="true"
             android:layout_marginTop="24dp"
             android:layout_marginEnd="24dp"
+            android:soundEffectsEnabled="false"
             android:visibility="gone"
             app:elevation="4dp" />
 
@@ -161,7 +163,8 @@
             <RelativeLayout
                 android:layout_width="288dp"
                 android:layout_height="wrap_content"
-                android:layout_centerInParent="true">
+                android:layout_centerInParent="true"
+                android:background="@android:color/transparent">
 
                 <com.google.android.material.floatingactionbutton.FloatingActionButton
                     android:id="@+id/reject_call"

src/main/res/values/strings.xml ๐Ÿ”—

@@ -1086,4 +1086,5 @@
     <string name="add_reaction">Add reactionโ€ฆ</string>
     <string name="add_reaction_title">Add reaction</string>
     <string name="more_reactions">More reactions</string>
+    <string name="could_not_modify_call">Could not modify call</string>
 </resources>