add switch to video menu item to call

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java | 139 ++
src/main/res/drawable/ic_baseline_check_24.xml                  |   5 
src/main/res/menu/activity_rtp_session.xml                      |   3 
src/main/res/values/strings.xml                                 |   4 
4 files changed, 132 insertions(+), 19 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java 🔗

@@ -65,6 +65,7 @@ import eu.siacs.conversations.utils.TimeFrameUtils;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
+import eu.siacs.conversations.xmpp.jingle.ContentAddition;
 import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
@@ -101,9 +102,12 @@ public class RtpSessionActivity extends XmppActivity
             Arrays.asList(
                     RtpEndUserState.CONNECTING,
                     RtpEndUserState.CONNECTED,
-                    RtpEndUserState.RECONNECTING);
+                    RtpEndUserState.RECONNECTING,
+                    RtpEndUserState.INCOMING_CONTENT_ADD);
     private static final List<RtpEndUserState> STATES_CONSIDERED_CONNECTED =
-            Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING);
+            Arrays.asList(
+                    RtpEndUserState.CONNECTED,
+                    RtpEndUserState.RECONNECTING);
     private static final List<RtpEndUserState> STATES_SHOWING_PIP_PLACEHOLDER =
             Arrays.asList(
                     RtpEndUserState.ACCEPTING_CALL,
@@ -111,6 +115,8 @@ public class RtpSessionActivity extends XmppActivity
                     RtpEndUserState.RECONNECTING);
     private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
     private static final int REQUEST_ACCEPT_CALL = 0x1111;
+    private static final int REQUEST_ACCEPT_CONTENT = 0x1112;
+    private static final int REQUEST_ADD_CONTENT = 0x1113;
     private WeakReference<JingleRtpConnection> rtpConnectionReference;
 
     private ActivityRtpSessionBinding binding;
@@ -164,8 +170,10 @@ public class RtpSessionActivity extends XmppActivity
         getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
         final MenuItem help = menu.findItem(R.id.action_help);
         final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat);
+        final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video);
         help.setVisible(Config.HELP != null && isHelpButtonVisible());
         gotoChat.setVisible(isSwitchToConversationVisible());
+        switchToVideo.setVisible(isSwitchToVideoVisible());
         return super.onCreateOptionsMenu(menu);
     }
 
@@ -203,6 +211,15 @@ public class RtpSessionActivity extends XmppActivity
                 && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState());
     }
 
+    private boolean isSwitchToVideoVisible() {
+        final JingleRtpConnection connection =
+                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
+        if (connection == null) {
+            return false;
+        }
+        return Media.audioOnly(connection.getMedia()) && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState());
+    }
+
     private void switchToConversation() {
         final Contact contact = getWith();
         final Conversation conversation =
@@ -215,10 +232,13 @@ public class RtpSessionActivity extends XmppActivity
         switch (item.getItemId()) {
             case R.id.action_help:
                 launchHelpInBrowser();
-                break;
+                return true;
             case R.id.action_goto_chat:
                 switchToConversation();
-                break;
+                return true;
+            case R.id.action_switch_to_video:
+                requestPermissionAndSwitchToVideo();
+                return true;
         }
         return super.onOptionsItemSelected(item);
     }
@@ -272,9 +292,60 @@ public class RtpSessionActivity extends XmppActivity
         requestPermissionsAndAcceptCall();
     }
 
+    private void acceptContentAdd() {
+        try {
+            requireRtpConnection()
+                    .acceptContentAdd(requireRtpConnection().getPendingContentAddition().summary);
+        } catch (final IllegalStateException e) {
+            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    private void requestPermissionAndSwitchToVideo() {
+        final List<String> permissions = permissions(ImmutableSet.of(Media.VIDEO, Media.AUDIO));
+        if (PermissionUtils.hasPermission(this, permissions, REQUEST_ADD_CONTENT)) {
+            switchToVideo();
+        }
+    }
+
+    private void switchToVideo() {
+        try {
+            requireRtpConnection().addMedia(Media.VIDEO);
+        } catch (final IllegalStateException e) {
+            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    private void acceptContentAdd(final ContentAddition contentAddition) {
+        if (contentAddition == null || contentAddition.direction != ContentAddition.Direction.INCOMING) {
+            Log.d(Config.LOGTAG,"ignore press on content-accept button");
+            return;
+        }
+        requestPermissionAndAcceptContentAdd(contentAddition);
+    }
+
+    private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) {
+        final List<String> permissions = permissions(contentAddition.media());
+        if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) {
+            requireRtpConnection().acceptContentAdd(contentAddition.summary);
+        }
+    }
+
+    private void rejectContentAdd(final View view) {
+        requireRtpConnection().rejectContentAdd();
+    }
+
     private void requestPermissionsAndAcceptCall() {
+        final List<String> permissions = permissions(getMedia());
+        if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
+            putScreenInCallMode();
+            checkRecorderAndAcceptCall();
+        }
+    }
+
+    private List<String> permissions(final Set<Media> media) {
         final ImmutableList.Builder<String> permissions = ImmutableList.builder();
-        if (getMedia().contains(Media.VIDEO)) {
+        if (media.contains(Media.VIDEO)) {
             permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO);
         } else {
             permissions.add(Manifest.permission.RECORD_AUDIO);
@@ -282,10 +353,7 @@ public class RtpSessionActivity extends XmppActivity
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
             permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
         }
-        if (PermissionUtils.hasPermission(this, permissions.build(), REQUEST_ACCEPT_CALL)) {
-            putScreenInCallMode();
-            checkRecorderAndAcceptCall();
-        }
+        return permissions.build();
     }
 
     private void checkRecorderAndAcceptCall() {
@@ -516,6 +584,10 @@ public class RtpSessionActivity extends XmppActivity
         if (PermissionUtils.allGranted(permissionResult.grantResults)) {
             if (requestCode == REQUEST_ACCEPT_CALL) {
                 checkRecorderAndAcceptCall();
+            } else if (requestCode == REQUEST_ACCEPT_CONTENT) {
+                acceptContentAdd();
+            } else if (requestCode == REQUEST_ADD_CONTENT) {
+                switchToVideo();
             }
         } else {
             @StringRes int res;
@@ -598,8 +670,8 @@ public class RtpSessionActivity extends XmppActivity
     private boolean isConnected() {
         final JingleRtpConnection connection =
                 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
-        return connection != null
-                && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState());
+        final RtpEndUserState endUserState = connection == null ? null : connection.getEndUserState();
+        return STATES_CONSIDERED_CONNECTED.contains(endUserState) || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD;
     }
 
     private boolean switchToPictureInPicture() {
@@ -691,6 +763,7 @@ public class RtpSessionActivity extends XmppActivity
             return true;
         }
         final Set<Media> media = getMedia();
+        final ContentAddition contentAddition = getPendingContentAddition();
         if (currentState == RtpEndUserState.INCOMING_CALL) {
             getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
         }
@@ -700,9 +773,9 @@ public class RtpSessionActivity extends XmppActivity
         }
         setWidth(currentState);
         updateVideoViews(currentState);
-        updateStateDisplay(currentState, media);
+        updateStateDisplay(currentState, media, contentAddition);
         updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState));
-        updateButtonConfiguration(currentState, media);
+        updateButtonConfiguration(currentState, media, contentAddition);
         updateIncomingCallScreen(currentState);
         invalidateOptionsMenu();
         return false;
@@ -753,10 +826,10 @@ public class RtpSessionActivity extends XmppActivity
     }
 
     private void updateStateDisplay(final RtpEndUserState state) {
-        updateStateDisplay(state, Collections.emptySet());
+        updateStateDisplay(state, Collections.emptySet(), null);
     }
 
-    private void updateStateDisplay(final RtpEndUserState state, final Set<Media> media) {
+    private void updateStateDisplay(final RtpEndUserState state, final Set<Media> media, final ContentAddition contentAddition) {
         switch (state) {
             case INCOMING_CALL:
                 Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
@@ -766,6 +839,13 @@ public class RtpSessionActivity extends XmppActivity
                     setTitle(R.string.rtp_state_incoming_call);
                 }
                 break;
+            case INCOMING_CONTENT_ADD:
+                if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) {
+                    setTitle(R.string.rtp_state_content_add_video);
+                } else {
+                    setTitle(R.string.rtp_state_content_add);
+                }
+                break;
             case CONNECTING:
                 setTitle(R.string.rtp_state_connecting);
                 break;
@@ -857,12 +937,16 @@ public class RtpSessionActivity extends XmppActivity
         return requireRtpConnection().getMedia();
     }
 
+    public ContentAddition getPendingContentAddition() {
+        return requireRtpConnection().getPendingContentAddition();
+    }
+
     private void updateButtonConfiguration(final RtpEndUserState state) {
-        updateButtonConfiguration(state, Collections.emptySet());
+        updateButtonConfiguration(state, Collections.emptySet(), null);
     }
 
     @SuppressLint("RestrictedApi")
-    private void updateButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
+    private void updateButtonConfiguration(final RtpEndUserState state, final Set<Media> media, final ContentAddition contentAddition) {
         if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
             this.binding.rejectCall.setVisibility(View.INVISIBLE);
             this.binding.endCall.setVisibility(View.INVISIBLE);
@@ -877,6 +961,16 @@ public class RtpSessionActivity extends XmppActivity
             this.binding.acceptCall.setOnClickListener(this::acceptCall);
             this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
             this.binding.acceptCall.setVisibility(View.VISIBLE);
+        } else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) {
+            this.binding.rejectCall.setContentDescription(getString(R.string.reject_switch_to_video));
+            this.binding.rejectCall.setOnClickListener(this::rejectContentAdd);
+            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
+            this.binding.rejectCall.setVisibility(View.VISIBLE);
+            this.binding.endCall.setVisibility(View.INVISIBLE);
+            this.binding.acceptCall.setContentDescription(getString(R.string.accept));
+            this.binding.acceptCall.setOnClickListener((v -> acceptContentAdd(contentAddition)));
+            this.binding.acceptCall.setImageResource(R.drawable.ic_baseline_check_24);
+            this.binding.acceptCall.setVisibility(View.VISIBLE);
         } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
             this.binding.rejectCall.setContentDescription(getString(R.string.exit));
             this.binding.rejectCall.setOnClickListener(this::exit);
@@ -1051,6 +1145,12 @@ public class RtpSessionActivity extends XmppActivity
     }
 
     private void disableVideo(View view) {
+        final JingleRtpConnection rtpConnection = requireRtpConnection();
+        final ContentAddition pending = rtpConnection.getPendingContentAddition();
+        if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) {
+            rtpConnection.retractContentAdd();
+            return;
+        }
         requireRtpConnection().setVideoEnabled(false);
         updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable());
     }
@@ -1279,6 +1379,7 @@ public class RtpSessionActivity extends XmppActivity
         final AbstractJingleConnection.Id id = requireRtpConnection().getId();
         final boolean verified = requireRtpConnection().isVerified();
         final Set<Media> media = getMedia();
+        final ContentAddition contentAddition = getPendingContentAddition();
         final Contact contact = getWith();
         if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
             if (state == RtpEndUserState.ENDED) {
@@ -1287,10 +1388,10 @@ public class RtpSessionActivity extends XmppActivity
             }
             runOnUiThread(
                     () -> {
-                        updateStateDisplay(state, media);
+                        updateStateDisplay(state, media, contentAddition);
                         updateVerifiedShield(
                                 verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state));
-                        updateButtonConfiguration(state, media);
+                        updateButtonConfiguration(state, media, contentAddition);
                         updateVideoViews(state);
                         updateIncomingCallScreen(state, contact);
                         invalidateOptionsMenu();

src/main/res/drawable/ic_baseline_check_24.xml 🔗

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>

src/main/res/menu/activity_rtp_session.xml 🔗

@@ -13,4 +13,7 @@
         android:icon="?attr/icon_goto_chat"
         android:title="@string/switch_to_conversation"
         app:showAsAction="always" />
+    <item android:id="@+id/action_switch_to_video"
+        android:title="@string/switch_to_video"
+        app:showAsAction="never"/>
 </menu>

src/main/res/values/strings.xml 🔗

@@ -909,6 +909,8 @@
     <string name="make_call">Make call</string>
     <string name="rtp_state_incoming_call">Incoming call</string>
     <string name="rtp_state_incoming_video_call">Incoming video call</string>
+    <string name="rtp_state_content_add_video">Switch to video call?</string>
+    <string name="rtp_state_content_add">Add additional tracks?</string>
     <string name="rtp_state_connecting">Connecting</string>
     <string name="rtp_state_connected">Connected</string>
     <string name="rtp_state_reconnecting">Reconnecting</string>
@@ -995,5 +997,7 @@
     <string name="account_status_temporary_auth_failure">Temporary authentication failure</string>
     <string name="delete_avatar">Delete avatar</string>
     <string name="audio_video_disabled_tor">Calls are disabled when using Tor</string>
+    <string name="switch_to_video">Switch to video</string>
+    <string name="reject_switch_to_video">Reject switch to video request</string>
 
 </resources>