瀏覽代碼

Merge pull request #1156 from nextcloud/techdebt/noid/api-v4-compatibility

v4️⃣ - Add option for APIv4 compatibility
Joas Schilling 4 年之前
父節點
當前提交
6434630709
共有 20 個文件被更改,包括 449 次插入211 次删除
  1. 3 0
      app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt
  2. 4 2
      app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
  3. 23 4
      app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java
  4. 29 11
      app/src/main/java/com/nextcloud/talk/controllers/CallController.java
  5. 29 53
      app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java
  6. 57 9
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  7. 14 4
      app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java
  8. 30 5
      app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt
  9. 4 1
      app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java
  10. 44 20
      app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java
  11. 6 4
      app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java
  12. 4 1
      app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java
  13. 6 1
      app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.java
  14. 2 1
      app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java
  15. 4 1
      app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java
  16. 5 2
      app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java
  17. 154 90
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  18. 23 0
      app/src/main/java/com/nextcloud/talk/utils/NoSupportedApiException.kt
  19. 5 1
      app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java
  20. 3 1
      app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java

+ 3 - 0
app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt

@@ -302,9 +302,12 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
         var hasParticipantsInCall = false
         var inCallOnDifferentDevice = false
 
+        val apiVersion = ApiUtils.getConversationApiVersion(signatureVerification.userEntity, intArrayOf(1))
+
         ncApi.getPeersForCall(
             ApiUtils.getCredentials(signatureVerification.userEntity.username, signatureVerification.userEntity.token),
             ApiUtils.getUrlForCall(
+                apiVersion,
                 signatureVerification.userEntity.baseUrl,
                 decryptedPushMessage.id
             )

+ 4 - 2
app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt

@@ -229,9 +229,10 @@ class MainActivity : BaseActivity(), ActionBarProvider {
         val roomType = "1"
         val currentUser = userUtils.currentUser ?: return
 
+        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(1))
         val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
         val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
-            currentUser.baseUrl, roomType,
+            apiVersion, currentUser.baseUrl, roomType,
             userId, null
         )
         ncApi.createRoom(
@@ -251,7 +252,8 @@ class MainActivity : BaseActivity(), ActionBarProvider {
                     if (currentUser.hasSpreedFeatureCapability("chat-v2")) {
                         ncApi.getRoom(
                             credentials,
-                            ApiUtils.getRoom(
+                            ApiUtils.getUrlForRoom(
+                                apiVersion,
                                 currentUser.baseUrl,
                                 roomOverall.ocs.data.token
                             )

+ 23 - 4
app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java

@@ -46,6 +46,7 @@ import com.nextcloud.talk.jobs.CapabilitiesWorker;
 import com.nextcloud.talk.jobs.PushRegistrationWorker;
 import com.nextcloud.talk.jobs.SignalingSettingsWorker;
 import com.nextcloud.talk.models.database.UserEntity;
+import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
 import com.nextcloud.talk.models.json.generic.Status;
 import com.nextcloud.talk.models.json.conversations.RoomsOverall;
 import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
@@ -221,17 +222,35 @@ public class AccountVerificationController extends BaseController {
     }
 
     private void findServerTalkApp(String credentials) {
-        ncApi.getRooms(credentials, ApiUtils.getUrlForGetRooms(baseUrl))
+        ncApi.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl))
                 .subscribeOn(Schedulers.io())
-                .subscribe(new Observer<RoomsOverall>() {
+                .subscribe(new Observer<CapabilitiesOverall>() {
                     @Override
                     public void onSubscribe(Disposable d) {
                         disposables.add(d);
                     }
 
                     @Override
-                    public void onNext(RoomsOverall roomsOverall) {
-                        fetchProfile(credentials);
+                    public void onNext(CapabilitiesOverall capabilitiesOverall) {
+                        boolean hasTalk =
+                                capabilitiesOverall.getOcs().getData().getCapabilities() != null
+                                        && capabilitiesOverall.getOcs().getData().getCapabilities().getSpreedCapability() != null
+                                        && capabilitiesOverall.getOcs().getData().getCapabilities().getSpreedCapability().getFeatures() != null
+                                        && !capabilitiesOverall.getOcs().getData().getCapabilities().getSpreedCapability().getFeatures().isEmpty();
+
+                        if (hasTalk) {
+                            fetchProfile(credentials);
+                        } else {
+                            if (getActivity() != null && getResources() != null) {
+                                getActivity().runOnUiThread(() -> progressText.setText(String.format(getResources().getString(
+                                        R.string.nc_nextcloud_talk_app_not_installed), getResources().getString(R.string.nc_app_name))));
+                            }
+
+                            ApplicationWideMessageHolder.getInstance().setMessageType(
+                                    ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK);
+
+                            abortVerification();
+                        }
                     }
 
                     @Override

+ 29 - 11
app/src/main/java/com/nextcloud/talk/controllers/CallController.java

@@ -441,7 +441,9 @@ public class CallController extends BaseController {
     }
 
     private void handleFromNotification() {
-        ncApi.getRooms(credentials, ApiUtils.getUrlForGetRooms(baseUrl))
+        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[] {1});
+
+        ncApi.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, baseUrl))
                 .retry(3)
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
@@ -1097,7 +1099,9 @@ public class CallController extends BaseController {
     }
 
     private void fetchSignalingSettings() {
-        ncApi.getSignalingSettings(credentials, ApiUtils.getUrlForSignalingSettings(baseUrl))
+        int apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, new int[] {2, 1});
+
+        ncApi.getSignalingSettings(credentials, ApiUtils.getUrlForSignalingSettings(apiVersion, baseUrl))
                 .subscribeOn(Schedulers.io())
                 .retry(3)
                 .observeOn(AndroidSchedulers.mainThread())
@@ -1234,9 +1238,11 @@ public class CallController extends BaseController {
     private void joinRoomAndCall() {
         callSession = ApplicationWideCurrentRoomHolder.getInstance().getSession();
 
+        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser,  new int[] {1});
+
         if (TextUtils.isEmpty(callSession)) {
-            ncApi.joinRoom(credentials, ApiUtils.getUrlForSettingMyselfAsActiveParticipant(baseUrl,
-                                                                                           roomToken), conversationPassword)
+            ncApi.joinRoom(credentials, ApiUtils.getUrlForParticipantsActive(apiVersion, baseUrl, roomToken),
+                           conversationPassword)
                     .subscribeOn(Schedulers.io())
                     .observeOn(AndroidSchedulers.mainThread())
                     .retry(3)
@@ -1288,8 +1294,9 @@ public class CallController extends BaseController {
             inCallFlag = (int) Participant.ParticipantFlags.IN_CALL_WITH_AUDIO_AND_VIDEO.getValue();
         }
 
-        ncApi.joinCall(credentials,
-                       ApiUtils.getUrlForCall(baseUrl, roomToken), inCallFlag)
+        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[] {1});
+
+        ncApi.joinCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken), inCallFlag)
                 .subscribeOn(Schedulers.io())
                 .retry(3)
                 .observeOn(AndroidSchedulers.mainThread())
@@ -1349,7 +1356,10 @@ public class CallController extends BaseController {
                             }
 
                             if (!hasExternalSignalingServer) {
-                                ncApi.pullSignalingMessages(credentials, ApiUtils.getUrlForSignaling(baseUrl, urlToken))
+                                int apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, new int[] {2, 1});
+
+                                ncApi.pullSignalingMessages(credentials, ApiUtils.getUrlForSignaling(apiVersion,
+                                                                                                     baseUrl, urlToken))
                                         .subscribeOn(Schedulers.io())
                                         .observeOn(AndroidSchedulers.mainThread())
                                         .repeatWhen(observable -> observable)
@@ -1613,7 +1623,9 @@ public class CallController extends BaseController {
     }
 
     private void hangupNetworkCalls(boolean shutDownView) {
-        ncApi.leaveCall(credentials, ApiUtils.getUrlForCall(baseUrl, roomToken))
+        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[] {1});
+
+        ncApi.leaveCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken))
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(new Observer<GenericOverall>() {
@@ -1648,7 +1660,9 @@ public class CallController extends BaseController {
     }
 
     private void leaveRoom(boolean shutDownView) {
-        ncApi.leaveRoom(credentials, ApiUtils.getUrlForSettingMyselfAsActiveParticipant(baseUrl, roomToken))
+        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[] {1});
+
+        ncApi.leaveRoom(credentials, ApiUtils.getUrlForParticipantsActive(apiVersion, baseUrl, roomToken))
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(new Observer<GenericOverall>() {
@@ -1741,7 +1755,9 @@ public class CallController extends BaseController {
 
     private void getPeersForCall() {
         Log.d(TAG, "getPeersForCall");
-        ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(baseUrl, roomToken))
+        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[] {1});
+
+        ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken))
                 .subscribeOn(Schedulers.io())
                 .subscribe(new Observer<ParticipantsOverall>() {
                     @Override
@@ -2045,7 +2061,9 @@ public class CallController extends BaseController {
                 urlToken = roomToken;
             }
 
-            ncApi.sendSignalingMessages(credentials, ApiUtils.getUrlForSignaling(baseUrl, urlToken),
+            int apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, new int[] {2, 1});
+
+            ncApi.sendSignalingMessages(credentials, ApiUtils.getUrlForSignaling(apiVersion, baseUrl, urlToken),
                                         strings.toString())
                     .retry(3)
                     .subscribeOn(Schedulers.io())

+ 29 - 53
app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java

@@ -209,7 +209,9 @@ public class CallNotificationController extends BaseController {
     }
 
     private void checkIfAnyParticipantsRemainInRoom() {
-        ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(userBeingCalled.getBaseUrl(),
+        int apiVersion = ApiUtils.getConversationApiVersion(userBeingCalled, new int[] {1});
+
+        ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(apiVersion, userBeingCalled.getBaseUrl(),
                                                                   currentConversation.getToken()))
                 .subscribeOn(Schedulers.io())
                 .takeWhile(observable -> !leavingScreen)
@@ -258,23 +260,24 @@ public class CallNotificationController extends BaseController {
     }
 
     private void handleFromNotification() {
-        boolean isConversationApiV3 = userBeingCalled.hasSpreedFeatureCapability("conversation-v3");
-        if (isConversationApiV3) {
-            ncApi.getRoom(credentials, ApiUtils.getRoomV3(userBeingCalled.getBaseUrl(), roomId))
-                    .subscribeOn(Schedulers.io())
-                    .retry(3)
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(new Observer<RoomOverall>() {
-                        @Override
-                        public void onSubscribe(Disposable d) {
-                            disposablesList.add(d);
-                        }
+        int apiVersion = ApiUtils.getConversationApiVersion(userBeingCalled, new int[] {4, 3, 1});
+
+        ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, userBeingCalled.getBaseUrl(), roomId))
+                .subscribeOn(Schedulers.io())
+                .retry(3)
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(new Observer<RoomOverall>() {
+                    @Override
+                    public void onSubscribe(Disposable d) {
+                        disposablesList.add(d);
+                    }
 
-                        @Override
-                        public void onNext(@NotNull RoomOverall roomOverall) {
-                            currentConversation = roomOverall.getOcs().data;
-                            runAllThings();
+                    @Override
+                    public void onNext(@NotNull RoomOverall roomOverall) {
+                        currentConversation = roomOverall.getOcs().data;
+                        runAllThings();
 
+                        if (apiVersion >= 3) {
                             boolean hasCallFlags = userBeingCalled.hasSpreedFeatureCapability("conversation-call-flags");
                             if (hasCallFlags) {
                                 if (isInCallWithVideo(currentConversation.callFlag)) {
@@ -286,46 +289,19 @@ public class CallNotificationController extends BaseController {
                                 }
                             }
                         }
+                    }
 
-                        @Override
-                        public void onError(Throwable e) {
-
-                        }
-
-                        @Override
-                        public void onComplete() {
-
-                        }
-                    });
-        } else {
-            ncApi.getRoom(credentials, ApiUtils.getRoom(userBeingCalled.getBaseUrl(), roomId))
-                    .subscribeOn(Schedulers.io())
-                    .retry(3)
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(new Observer<RoomOverall>() {
-                        @Override
-                        public void onSubscribe(Disposable d) {
-                            disposablesList.add(d);
-                        }
-
-                        @SuppressLint("LongLogTag")
-                        @Override
-                        public void onNext(@NotNull RoomOverall roomOverall) {
-                            currentConversation = roomOverall.getOcs().data;
-                            runAllThings();
-                        }
-
-                        @Override
-                        public void onError(Throwable e) {
-
-                        }
+                    @SuppressLint("LongLogTag")
+                    @Override
+                    public void onError(Throwable e) {
+                        Log.e(TAG, e.getMessage(), e);
+                    }
 
-                        @Override
-                        public void onComplete() {
+                    @Override
+                    public void onComplete() {
 
-                        }
-                    });
-        }
+                    }
+                });
     }
 
     private boolean isInCallWithVideo(int callFlag) {

+ 57 - 9
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -293,7 +293,9 @@ class ChatController(args: Bundle) :
         }
 
         if (conversationUser != null) {
-            ncApi?.getRoom(credentials, ApiUtils.getRoom(conversationUser.baseUrl, roomToken))
+            val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+
+            ncApi?.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser.baseUrl, roomToken))
                 ?.subscribeOn(Schedulers.io())
                 ?.observeOn(AndroidSchedulers.mainThread())
                 ?.subscribe(object : Observer<RoomOverall> {
@@ -332,7 +334,13 @@ class ChatController(args: Bundle) :
     }
 
     private fun handleFromNotification() {
-        ncApi?.getRooms(credentials, ApiUtils.getUrlForGetRooms(conversationUser?.baseUrl))
+        var apiVersion = 1
+        // FIXME Can this be called for guests?
+        if (conversationUser != null) {
+            apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+        }
+
+        ncApi?.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, conversationUser?.baseUrl))
             ?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<RoomsOverall> {
                 override fun onSubscribe(d: Disposable) {
@@ -958,9 +966,16 @@ class ChatController(args: Bundle) :
         if (currentConversation == null || TextUtils.isEmpty(currentConversation?.sessionId) ||
             currentConversation?.sessionId == "0"
         ) {
+            var apiVersion = 1
+            // FIXME Fix API checking with guests?
+            if (conversationUser != null) {
+                apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+            }
+
             ncApi?.joinRoom(
                 credentials,
-                ApiUtils.getUrlForSettingMyselfAsActiveParticipant(conversationUser?.baseUrl, roomToken), roomPassword
+                ApiUtils.getUrlForParticipantsActive(apiVersion, conversationUser?.baseUrl, roomToken),
+                roomPassword
             )
                 ?.subscribeOn(Schedulers.io())
                 ?.observeOn(AndroidSchedulers.mainThread())
@@ -1024,9 +1039,16 @@ class ChatController(args: Bundle) :
     }
 
     private fun leaveRoom() {
+        var apiVersion = 1
+        // FIXME Fix API checking with guests?
+        if (conversationUser != null) {
+            apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+        }
+
         ncApi?.leaveRoom(
             credentials,
-            ApiUtils.getUrlForSettingMyselfAsActiveParticipant(
+            ApiUtils.getUrlForParticipantsActive(
+                apiVersion,
                 conversationUser?.baseUrl,
                 roomToken
             )
@@ -1107,9 +1129,11 @@ class ChatController(args: Bundle) :
     private fun sendMessage(message: CharSequence, replyTo: Int?) {
 
         if (conversationUser != null) {
+            val apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
+
             ncApi!!.sendChatMessage(
                 credentials,
-                ApiUtils.getUrlForChat(conversationUser.baseUrl, roomToken),
+                ApiUtils.getUrlForChat(apiVersion, conversationUser.baseUrl, roomToken),
                 message,
                 conversationUser.displayName,
                 replyTo
@@ -1209,11 +1233,17 @@ class ChatController(args: Bundle) :
         }
 
         if (!wasDetached) {
+            var apiVersion = 1
+            // FIXME this is a best guess, guests would need to get the capabilities themselves
+            if (conversationUser != null) {
+                apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
+            }
+
             if (lookIntoFuture > 0) {
                 val finalTimeout = timeout
                 ncApi?.pullChatMessages(
                     credentials,
-                    ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap
+                    ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken), fieldMap
                 )
                     ?.subscribeOn(Schedulers.io())
                     ?.observeOn(AndroidSchedulers.mainThread())
@@ -1242,7 +1272,7 @@ class ChatController(args: Bundle) :
             } else {
                 ncApi?.pullChatMessages(
                     credentials,
-                    ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap
+                    ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken), fieldMap
                 )
                     ?.subscribeOn(Schedulers.io())
                     ?.observeOn(AndroidSchedulers.mainThread())
@@ -1632,9 +1662,20 @@ class ChatController(args: Bundle) :
                         true
                     }
                     R.id.action_delete_message -> {
+                        var apiVersion = 1
+                        // FIXME Fix API checking with guests?
+                        if (conversationUser != null) {
+                            apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+                        }
+
                         ncApi?.deleteChatMessage(
                             credentials,
-                            ApiUtils.getUrlForMessageDeletion(conversationUser?.baseUrl, roomToken, message?.id)
+                            ApiUtils.getUrlForChatMessage(
+                                apiVersion,
+                                conversationUser?.baseUrl,
+                                roomToken,
+                                message?.id
+                            )
                         )?.subscribeOn(Schedulers.io())
                             ?.observeOn(AndroidSchedulers.mainThread())
                             ?.subscribe(object : Observer<ChatOverallSingleMessage> {
@@ -1743,8 +1784,15 @@ class ChatController(args: Bundle) :
         if (currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
             currentConversation?.name != userMentionClickEvent.userId
         ) {
+
+            var apiVersion = 1
+            // FIXME Fix API checking with guests?
+            if (conversationUser != null) {
+                apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+            }
+
             val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
-                conversationUser?.baseUrl, "1",
+                apiVersion, conversationUser?.baseUrl, "1",
                 userMentionClickEvent.userId, null
             )
 

+ 14 - 4
app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java

@@ -288,8 +288,12 @@ public class ContactsController extends BaseController implements SearchView.OnQ
                     userId = selectedUserIds.iterator().next();
                 }
 
-                RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(currentUser.getBaseUrl(), roomType,
-                        userId, null);
+                int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {1});
+                RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(apiVersion,
+                                                                                        currentUser.getBaseUrl(),
+                                                                                        roomType,
+                                                                                        userId,
+                                                                                        null);
                 ncApi.createRoom(credentials,
                         retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
                         .subscribeOn(Schedulers.io())
@@ -310,7 +314,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ
 
                                 if (currentUser.hasSpreedFeatureCapability("chat-v2")) {
                                     ncApi.getRoom(credentials,
-                                            ApiUtils.getRoom(currentUser.getBaseUrl(),
+                                            ApiUtils.getUrlForRoom(apiVersion, currentUser.getBaseUrl(),
                                                     roomOverall.getOcs().getData().getToken()))
                                             .subscribeOn(Schedulers.io())
                                             .observeOn(AndroidSchedulers.mainThread())
@@ -848,7 +852,13 @@ public class ContactsController extends BaseController implements SearchView.OnQ
                     roomType = "2";
                 }
 
-                RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(currentUser.getBaseUrl(), roomType, userItem.getModel().getUserId(), null);
+                int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {1});
+
+                RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(apiVersion,
+                                                                                        currentUser.getBaseUrl(),
+                                                                                        roomType,
+                                                                                        userItem.getModel().getUserId(),
+                                                                                        null);
 
                 ncApi.createRoom(credentials,
                         retrofitBucket.getUrl(), retrofitBucket.getQueryMap())

+ 30 - 5
app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt

@@ -306,9 +306,12 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                 ) as SwitchCompat
                 ).isChecked
         ) 1 else 0
+
+        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+
         ncApi.setLobbyForConversation(
             ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token),
-            ApiUtils.getUrlForLobbyForConversation(conversationUser.baseUrl, conversation!!.token),
+            ApiUtils.getUrlForRoomWebinaryLobby(apiVersion, conversationUser.baseUrl, conversation!!.token),
             state,
             conversation!!.lobbyTimer
         )
@@ -438,9 +441,15 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
     }
 
     private fun getListOfParticipants() {
+        var apiVersion = 1
+        // FIXME Fix API checking with guests?
+        if (conversationUser != null) {
+            apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+        }
+
         ncApi.getPeersForCall(
             credentials,
-            ApiUtils.getUrlForParticipants(conversationUser!!.baseUrl, conversationToken)
+            ApiUtils.getUrlForParticipants(apiVersion, conversationUser!!.baseUrl, conversationToken)
         )
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
@@ -527,7 +536,13 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
     }
 
     private fun fetchRoomInfo() {
-        ncApi.getRoom(credentials, ApiUtils.getRoom(conversationUser!!.baseUrl, conversationToken))
+        var apiVersion = 1
+        // FIXME Fix API checking with guests?
+        if (conversationUser != null) {
+            apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+        }
+
+        ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser!!.baseUrl, conversationToken))
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
             .subscribe(object : Observer<RoomOverall> {
@@ -701,11 +716,17 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                     title(text = participant.displayName)
                     listItemsWithImage(items = items) { dialog, index, _ ->
 
+                        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+
                         if (index == 0) {
                             if (participant.type == Participant.ParticipantType.MODERATOR) {
                                 ncApi.demoteModeratorToUser(
                                     credentials,
-                                    ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token),
+                                    ApiUtils.getUrlForRoomModerators(
+                                        apiVersion,
+                                        conversationUser.baseUrl,
+                                        conversation!!.token
+                                    ),
                                     participant.userId
                                 )
                                     .subscribeOn(Schedulers.io())
@@ -716,7 +737,11 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                             } else if (participant.type == Participant.ParticipantType.USER) {
                                 ncApi.promoteUserToModerator(
                                     credentials,
-                                    ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token),
+                                    ApiUtils.getUrlForRoomModerators(
+                                        apiVersion,
+                                        conversationUser.baseUrl,
+                                        conversation!!.token
+                                    ),
                                     participant.userId
                                 )
                                     .subscribeOn(Schedulers.io())

+ 4 - 1
app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java

@@ -418,7 +418,10 @@ public class ConversationsListController extends BaseController implements Searc
 
         callItems = new ArrayList<>();
 
-        roomsQueryDisposable = ncApi.getRooms(credentials, ApiUtils.getUrlForGetRooms(currentUser.getBaseUrl()))
+        int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {4, 1});
+
+        roomsQueryDisposable = ncApi.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion,
+                                                                                   currentUser.getBaseUrl()))
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(roomsOverall -> {

+ 44 - 20
app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java

@@ -185,13 +185,20 @@ public class OperationsMenuController extends BaseController {
         if (currentUser != null) {
             credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken());
 
+            int apiVersion;
             if (!TextUtils.isEmpty(baseUrl) && !baseUrl.equals(currentUser.getBaseUrl())) {
                 credentials = null;
+                // FIXME joining a public link we need to check other capabilities
+                apiVersion = 1;
+            } else {
+                apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {1});
             }
 
+
             switch (operationCode) {
                 case 2:
-                    ncApi.renameRoom(credentials, ApiUtils.getRoom(currentUser.getBaseUrl(), conversation.getToken()),
+                    ncApi.renameRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, currentUser.getBaseUrl(),
+                                                                         conversation.getToken()),
                             conversation.getName())
                             .subscribeOn(Schedulers.io())
                             .observeOn(AndroidSchedulers.mainThread())
@@ -199,8 +206,8 @@ public class OperationsMenuController extends BaseController {
                             .subscribe(operationsObserver);
                     break;
                 case 3:
-                    ncApi.makeRoomPublic(credentials, ApiUtils.getUrlForRoomVisibility(currentUser.getBaseUrl(), conversation
-                            .getToken()))
+                    ncApi.makeRoomPublic(credentials, ApiUtils.getUrlForRoomPublic(apiVersion, currentUser.getBaseUrl(),
+                                                                                   conversation.getToken()))
                             .subscribeOn(Schedulers.io())
                             .observeOn(AndroidSchedulers.mainThread())
                             .retry(1)
@@ -213,8 +220,8 @@ public class OperationsMenuController extends BaseController {
                     if (conversation.getPassword() != null) {
                         pass = conversation.getPassword();
                     }
-                    ncApi.setPassword(credentials, ApiUtils.getUrlForPassword(currentUser.getBaseUrl(),
-                            conversation.getToken()), pass)
+                    ncApi.setPassword(credentials, ApiUtils.getUrlForRoomPassword(apiVersion, currentUser.getBaseUrl(),
+                                                                                  conversation.getToken()), pass)
                             .subscribeOn(Schedulers.io())
                             .observeOn(AndroidSchedulers.mainThread())
                             .retry(1)
@@ -224,15 +231,16 @@ public class OperationsMenuController extends BaseController {
                     // Operation 7 is sharing, so we handle this differently
                     break;
                 case 8:
-                    ncApi.makeRoomPrivate(credentials, ApiUtils.getUrlForRoomVisibility(currentUser.getBaseUrl(), conversation
-                            .getToken()))
+                    ncApi.makeRoomPrivate(credentials, ApiUtils.getUrlForRoomPublic(apiVersion,
+                                                                                    currentUser.getBaseUrl(),
+                                                                                    conversation.getToken()))
                             .subscribeOn(Schedulers.io())
                             .observeOn(AndroidSchedulers.mainThread())
                             .retry(1)
                             .subscribe(operationsObserver);
                     break;
                 case 10:
-                    ncApi.getRoom(credentials, ApiUtils.getRoom(baseUrl, conversationToken))
+                    ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, baseUrl, conversationToken))
                             .subscribeOn(Schedulers.io())
                             .observeOn(AndroidSchedulers.mainThread())
                             .retry(1)
@@ -271,7 +279,7 @@ public class OperationsMenuController extends BaseController {
 
                     if (conversationType.equals(Conversation.ConversationType.ROOM_PUBLIC_CALL) ||
                             !currentUser.hasSpreedFeatureCapability("empty-group-room")) {
-                        retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(currentUser.getBaseUrl(),
+                        retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(apiVersion, currentUser.getBaseUrl(),
                                 "3", invite, conversationName);
                     } else {
                         String roomType = "2";
@@ -280,7 +288,7 @@ public class OperationsMenuController extends BaseController {
                             roomType = "3";
                         }
 
-                        retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(currentUser.getBaseUrl(),
+                        retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(apiVersion, currentUser.getBaseUrl(),
                                 roomType, invite, conversationName);
                     }
 
@@ -300,7 +308,8 @@ public class OperationsMenuController extends BaseController {
                                     conversation = roomOverall.getOcs().getData();
 
                                     ncApi.getRoom(credentials,
-                                            ApiUtils.getRoom(currentUser.getBaseUrl(), conversation.getToken()))
+                                            ApiUtils.getUrlForRoom(apiVersion, currentUser.getBaseUrl(),
+                                                             conversation.getToken()))
                                             .subscribeOn(Schedulers.io())
                                             .observeOn(AndroidSchedulers.mainThread())
                                             .subscribe(new Observer<RoomOverall>() {
@@ -349,15 +358,19 @@ public class OperationsMenuController extends BaseController {
                 case 97:
                 case 98:
                     if (operationCode == 97) {
-                        ncApi.removeConversationFromFavorites(credentials, ApiUtils.getUrlForConversationFavorites(currentUser.getBaseUrl(),
-                                conversation.getToken()))
+                        ncApi.removeConversationFromFavorites(credentials,
+                                                              ApiUtils.getUrlForRoomFavorite(apiVersion,
+                                                                                             currentUser.getBaseUrl(),
+                                                                                             conversation.getToken()))
                                 .subscribeOn(Schedulers.io())
                                 .observeOn(AndroidSchedulers.mainThread())
                                 .retry(1)
                                 .subscribe(operationsObserver);
                     } else {
-                        ncApi.addConversationToFavorites(credentials, ApiUtils.getUrlForConversationFavorites(currentUser.getBaseUrl(),
-                                conversation.getToken()))
+                        ncApi.addConversationToFavorites(credentials,
+                                                         ApiUtils.getUrlForRoomFavorite(apiVersion,
+                                                                                        currentUser.getBaseUrl(),
+                                                                                        conversation.getToken()))
                                 .subscribeOn(Schedulers.io())
                                 .observeOn(AndroidSchedulers.mainThread())
                                 .retry(1)
@@ -365,7 +378,9 @@ public class OperationsMenuController extends BaseController {
                     }
                     break;
                 case 99:
-                    ncApi.joinRoom(credentials, ApiUtils.getUrlForSettingMyselfAsActiveParticipant(baseUrl, conversationToken),
+                    ncApi.joinRoom(credentials, ApiUtils.getUrlForParticipantsActive(apiVersion,
+                                                                                     baseUrl,
+                                                                                     conversationToken),
                             callPassword)
                             .subscribeOn(Schedulers.io())
                             .observeOn(AndroidSchedulers.mainThread())
@@ -379,7 +394,10 @@ public class OperationsMenuController extends BaseController {
     }
 
     private void performGroupCallWorkaround(String credentials) {
-        ncApi.makeRoomPrivate(credentials, ApiUtils.getUrlForRoomVisibility(currentUser.getBaseUrl(), conversation.getToken()))
+        int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {1});
+
+        ncApi.makeRoomPrivate(credentials, ApiUtils.getUrlForRoomPublic(apiVersion, currentUser.getBaseUrl(),
+                                                                            conversation.getToken()))
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
                 .retry(1)
@@ -534,11 +552,15 @@ public class OperationsMenuController extends BaseController {
             localInvitedGroups.remove(0);
         }
 
+        int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {1});
+
         if (localInvitedUsers.size() > 0 || (localInvitedGroups.size() > 0 && currentUser.hasSpreedFeatureCapability("invite-groups-and-mails"))) {
             if ((localInvitedGroups.size() > 0 && currentUser.hasSpreedFeatureCapability("invite-groups-and-mails"))) {
                 for (int i = 0; i < localInvitedGroups.size(); i++) {
                     final String groupId = localInvitedGroups.get(i);
-                    retrofitBucket = ApiUtils.getRetrofitBucketForAddGroupParticipant(currentUser.getBaseUrl(), conversation.getToken(),
+                    retrofitBucket = ApiUtils.getRetrofitBucketForAddGroupParticipant(apiVersion,
+                                                                                      currentUser.getBaseUrl(),
+                                                                                      conversation.getToken(),
                             groupId);
 
                     ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
@@ -578,8 +600,10 @@ public class OperationsMenuController extends BaseController {
 
             for (int i = 0; i < localInvitedUsers.size(); i++) {
                 final String userId = invitedUsers.get(i);
-                retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipant(currentUser.getBaseUrl(), conversation.getToken(),
-                        userId);
+                retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipant(apiVersion,
+                                                                             currentUser.getBaseUrl(),
+                                                                             conversation.getToken(),
+                                                                             userId);
 
                 ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
                         .subscribeOn(Schedulers.io())

+ 6 - 4
app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java

@@ -33,8 +33,6 @@ import com.nextcloud.talk.utils.database.user.UserUtils;
 
 import org.greenrobot.eventbus.EventBus;
 
-import java.util.ArrayList;
-
 import javax.inject.Inject;
 
 import androidx.annotation.NonNull;
@@ -67,12 +65,16 @@ public class AddParticipantsToConversation extends Worker {
         String[] selectedUserIds = data.getStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_USERS());
         String[] selectedGroupIds = data.getStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_GROUPS());
         UserEntity user = userUtils.getUserWithInternalId(data.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), -1));
+
+        int apiVersion = ApiUtils.getConversationApiVersion(user, new int[] {1});
+
         String conversationToken = data.getString(BundleKeys.INSTANCE.getKEY_TOKEN());
         String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken());
 
         RetrofitBucket retrofitBucket;
         for (String userId : selectedUserIds) {
-            retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipant(user.getBaseUrl(), conversationToken,
+            retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipant(apiVersion, user.getBaseUrl(),
+                                                                         conversationToken,
                     userId);
 
             ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
@@ -81,7 +83,7 @@ public class AddParticipantsToConversation extends Worker {
         }
 
         for (String groupId : selectedGroupIds) {
-            retrofitBucket = ApiUtils.getRetrofitBucketForAddGroupParticipant(user.getBaseUrl(), conversationToken,
+            retrofitBucket = ApiUtils.getRetrofitBucketForAddGroupParticipant(apiVersion, user.getBaseUrl(), conversationToken,
                     groupId);
 
             ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap())

+ 4 - 1
app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java

@@ -75,6 +75,8 @@ public class DeleteConversationWorker extends Worker {
         UserEntity operationUser = userUtils.getUserWithId(operationUserId);
 
         if (operationUser != null) {
+            int apiVersion = ApiUtils.getConversationApiVersion(operationUser,  new int[] {1});
+
             String credentials = ApiUtils.getCredentials(operationUser.getUsername(), operationUser.getToken());
             ncApi = retrofit.newBuilder().client(okHttpClient.newBuilder().cookieJar(new
                     JavaNetCookieJar(new CookieManager())).build()).build().create(NcApi.class);
@@ -82,7 +84,8 @@ public class DeleteConversationWorker extends Worker {
             EventStatus eventStatus = new EventStatus(operationUser.getId(),
                     EventStatus.EventType.CONVERSATION_UPDATE, true);
 
-            ncApi.deleteRoom(credentials, ApiUtils.getRoom(operationUser.getBaseUrl(), conversationToken))
+            ncApi.deleteRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, operationUser.getBaseUrl(),
+                                                              conversationToken))
                     .subscribeOn(Schedulers.io())
                     .blockingSubscribe(new Observer<GenericOverall>() {
                         Disposable disposable;

+ 6 - 1
app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.java

@@ -47,6 +47,7 @@ import java.net.CookieManager;
 
 @AutoInjector(NextcloudTalkApplication.class)
 public class LeaveConversationWorker extends Worker {
+
     @Inject
     Retrofit retrofit;
 
@@ -82,7 +83,11 @@ public class LeaveConversationWorker extends Worker {
             EventStatus eventStatus = new EventStatus(operationUser.getId(),
                     EventStatus.EventType.CONVERSATION_UPDATE, true);
 
-            ncApi.removeSelfFromRoom(credentials, ApiUtils.getUrlForRemoveSelfFromRoom(operationUser.getBaseUrl(), conversationToken))
+            int apiVersion = ApiUtils.getConversationApiVersion(operationUser, new int[] {1});
+
+            ncApi.removeSelfFromRoom(credentials, ApiUtils.getUrlForParticipantsSelf(apiVersion,
+                                                                                     operationUser.getBaseUrl(),
+                                                                                     conversationToken))
                     .subscribeOn(Schedulers.io())
                     .blockingSubscribe(new Observer<GenericOverall>() {
                         Disposable disposable;

+ 2 - 1
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java

@@ -152,8 +152,9 @@ public class NotificationWorker extends Worker {
             importantConversation = Boolean.parseBoolean(arbitraryStorageEntity.getValue());
         }
 
+        int apiVersion = ApiUtils.getConversationApiVersion(userEntity, new int[] {1});
 
-        ncApi.getRoom(credentials, ApiUtils.getRoom(userEntity.getBaseUrl(),
+        ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, userEntity.getBaseUrl(),
                 intent.getExtras().getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN())))
                 .blockingSubscribe(new Observer<RoomOverall>() {
                     @Override

+ 4 - 1
app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java

@@ -81,8 +81,11 @@ public class SignalingSettingsWorker extends Worker {
         for (int i = 0; i < userEntityList.size(); i++) {
             userEntity = userEntityList.get(i);
             UserEntity finalUserEntity = userEntity;
+
+            int apiVersion = ApiUtils.getSignalingApiVersion(finalUserEntity, new int[] {2, 1});
+
             ncApi.getSignalingSettings(ApiUtils.getCredentials(userEntity.getUsername(), userEntity.getToken()),
-                    ApiUtils.getUrlForSignalingSettings(userEntity.getBaseUrl()))
+                    ApiUtils.getUrlForSignalingSettings(apiVersion, userEntity.getBaseUrl()))
                     .blockingSubscribe(new Observer<SignalingSettingsOverall>() {
                         @Override
                         public void onSubscribe(Disposable d) {

+ 5 - 2
app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java

@@ -91,9 +91,12 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
             queryString = "";
         }
 
+        int apiVersion = ApiUtils.getChatApiVersion(currentUser, new int[] {1});
+
         adapter.setFilter(queryString);
-        ncApi.getMentionAutocompleteSuggestions(ApiUtils.getCredentials(currentUser.getUsername(), currentUser
-                        .getToken()), ApiUtils.getUrlForMentionSuggestions(currentUser.getBaseUrl(), roomToken),
+        ncApi.getMentionAutocompleteSuggestions(
+                ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
+                ApiUtils.getUrlForMentionSuggestions(apiVersion, currentUser.getBaseUrl(), roomToken),
                 queryString, 5)
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())

+ 154 - 90
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -21,11 +21,13 @@ package com.nextcloud.talk.utils;
 
 import android.net.Uri;
 import android.text.TextUtils;
+import android.util.Log;
 
 import com.nextcloud.talk.BuildConfig;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.models.RetrofitBucket;
+import com.nextcloud.talk.models.database.UserEntity;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -35,21 +37,24 @@ import androidx.annotation.Nullable;
 import okhttp3.Credentials;
 
 public class ApiUtils {
-    private static String ocsApiVersion = "/ocs/v2.php";
-    private static String spreedApiVersion = "/apps/spreed/api/v1";
+    private static final String TAG = "ApiUtils";
+    private static final String ocsApiVersion = "/ocs/v2.php";
+    private static final String spreedApiVersion = "/apps/spreed/api/v1";
+    private static final String spreedApiBase = ocsApiVersion + "/apps/spreed/api/v";
 
-    private static String userAgent = "Mozilla/5.0 (Android) Nextcloud-Talk v";
+    private static final String userAgent = "Mozilla/5.0 (Android) Nextcloud-Talk v";
 
     public static String getUserAgent() {
         return userAgent + BuildConfig.VERSION_NAME;
     }
 
-    public static String getUrlForLobbyForConversation(String baseUrl, String token) {
-        return getRoom(baseUrl, token) + "/webinary/lobby";
-    }
-
+    /**
+     * @deprecated This is only supported on API v1-3, in API v4+ please use
+     * {@link ApiUtils#getUrlForAttendees(int, String, String)} instead.
+     */
+    @Deprecated
     public static String getUrlForRemovingParticipantFromConversation(String baseUrl, String roomToken, boolean isGuest) {
-        String url = getUrlForParticipants(baseUrl, roomToken);
+        String url = getUrlForParticipants(1, baseUrl, roomToken);
 
         if (isGuest) {
             url += "/guests";
@@ -100,41 +105,154 @@ public class ApiUtils {
         return retrofitBucket;
     }
 
+    public static String getUrlForCapabilities(String baseUrl) {
+        return baseUrl + ocsApiVersion + "/cloud/capabilities";
+    }
+
+    public static int getConversationApiVersion(UserEntity capabilities, int[] versions) throws NoSupportedApiException {
+        boolean hasApiV4 = false;
+        for (int version : versions) {
+            hasApiV4 |= version == 4;
+        }
+
+        if (!hasApiV4) {
+            Exception e = new Exception("Api call did not try conversation-v4 api");
+            Log.d(TAG, e.getMessage(), e);
+        }
+
+        for (int version : versions) {
+            if (capabilities.hasSpreedFeatureCapability("conversation-v" + version)) {
+                return version;
+            }
+
+            // Fallback for old API versions
+            if ((version == 1 || version == 2)) {
+                if (capabilities.hasSpreedFeatureCapability("conversation-v2")) {
+                    return version;
+                }
+                if (version == 1  && capabilities.hasSpreedFeatureCapability("conversation")) {
+                    return version;
+                }
+            }
+        }
+        throw new NoSupportedApiException();
+    }
+
+    public static int getSignalingApiVersion(UserEntity capabilities, int[] versions) throws NoSupportedApiException {
+        for (int version : versions) {
+            if (version == 2 && capabilities.hasSpreedFeatureCapability("sip-support")) {
+                return version;
+            }
+
+            if (version == 1) {
+                // Has no capability, we just assume it is always there for now.
+                return version;
+            }
+        }
+        throw new NoSupportedApiException();
+    }
 
-    public static String getUrlForSettingNotificationlevel(String baseUrl, String token) {
-        return getRoom(baseUrl, token) + "/notify";
+    public static int getChatApiVersion(UserEntity capabilities, int[] versions) throws NoSupportedApiException {
+        for (int version : versions) {
+            if (version == 1 && capabilities.hasSpreedFeatureCapability("chat-v2")) {
+                // Do not question that chat-v2 capability shows the availability of api/v1/ endpoint *see no evil*
+                return version;
+            }
+        }
+        throw new NoSupportedApiException();
     }
 
-    public static String getUrlForSettingMyselfAsActiveParticipant(String baseUrl, String token) {
-        return getRoom(baseUrl, token) + "/participants/active";
+    protected static String getUrlForApi(int version, String baseUrl) {
+        return baseUrl + spreedApiBase + version;
     }
 
+    public static String getUrlForRooms(int version, String baseUrl) {
+        return getUrlForApi(version, baseUrl) + "/room";
+    }
 
-    public static String getUrlForParticipants(String baseUrl, String token) {
-        return getRoom(baseUrl, token) + "/participants";
+    public static String getUrlForRoom(int version, String baseUrl, String token) {
+        return getUrlForRooms(version, baseUrl) + "/" + token;
     }
 
-    public static String getUrlForCapabilities(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/cloud/capabilities";
+    public static String getUrlForAttendees(int version, String baseUrl, String token) {
+        return getUrlForRoom(version, baseUrl, token) + "/attendees";
     }
 
-    public static String getUrlForGetRooms(String baseUrl) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/room";
+    public static String getUrlForParticipants(int version, String baseUrl, String token) {
+        return getUrlForRoom(version, baseUrl, token) + "/participants";
     }
 
-    public static String getRoom(String baseUrl, String token) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + token;
+    public static String getUrlForParticipantsActive(int version, String baseUrl, String token) {
+        return getUrlForParticipants(version, baseUrl, token) + "/active";
     }
 
-    public static String getRoomV3(String baseUrl, String token) {
-        return baseUrl + ocsApiVersion + "/apps/spreed/api/v3" + "/room/" + token;
+    public static String getUrlForParticipantsSelf(int version, String baseUrl, String token) {
+        return getUrlForParticipants(version, baseUrl, token) + "/self";
     }
 
-    public static RetrofitBucket getRetrofitBucketForCreateRoom(String baseUrl, String roomType,
+    public static String getUrlForRoomFavorite(int version, String baseUrl, String token) {
+        return getUrlForRoom(version, baseUrl, token) + "/favorite";
+    }
+
+    public static String getUrlForRoomModerators(int version, String baseUrl, String token) {
+        return getUrlForRoom(version, baseUrl, token) + "/moderators";
+    }
+
+    public static String getUrlForRoomNotificationLevel(int version, String baseUrl, String token) {
+        return getUrlForRoom(version, baseUrl, token) + "/notify";
+    }
+
+    public static String getUrlForRoomPublic(int version, String baseUrl, String token) {
+        return getUrlForRoom(version, baseUrl, token) + "/public";
+    }
+
+    public static String getUrlForRoomPassword(int version, String baseUrl, String token) {
+        return getUrlForRoom(version, baseUrl, token) + "/password";
+    }
+
+    public static String getUrlForRoomReadOnlyState(int version, String baseUrl, String token) {
+        return getUrlForRoom(version, baseUrl, token) + "/read-only";
+    }
+
+    public static String getUrlForRoomWebinaryLobby(int version, String baseUrl, String token) {
+        return getUrlForRoom(version, baseUrl, token) + "/webinary/lobby";
+    }
+
+    public static String getUrlForCall(int version, String baseUrl, String token) {
+        return getUrlForApi(version, baseUrl) + "/call/" + token;
+    }
+    public static String getUrlForChat(int version, String baseUrl, String token) {
+        return getUrlForApi(version, baseUrl) + "/chat/" + token;
+    }
+
+    public static String getUrlForMentionSuggestions(int version, String baseUrl, String token) {
+        return getUrlForChat(version, baseUrl, token) + "/mentions";
+    }
+    public static String getUrlForChatMessage(int version, String baseUrl, String token, String messageId) {
+        return getUrlForChat(version, baseUrl, token) + "/" + messageId;
+    }
+
+    public static String getUrlForSignaling(int version, String baseUrl) {
+        return getUrlForApi(version, baseUrl) + "/signaling";
+    }
+
+    public static String getUrlForSignalingBackend(int version, String baseUrl) {
+        return getUrlForSignaling(version, baseUrl) + "/backend";
+    }
+
+    public static String getUrlForSignalingSettings(int version, String baseUrl) {
+        return getUrlForSignaling(version, baseUrl) + "/settings";
+    }
+
+    public static String getUrlForSignaling(int version, String baseUrl, String token) {
+        return getUrlForSignaling(version, baseUrl) + "/" + token;
+    }
+
+    public static RetrofitBucket getRetrofitBucketForCreateRoom(int version, String baseUrl, String roomType,
                                                                 @Nullable String invite,
                                                                 @Nullable String conversationName) {
         RetrofitBucket retrofitBucket = new RetrofitBucket();
-        retrofitBucket.setUrl(baseUrl + ocsApiVersion + spreedApiVersion + "/room");
+        retrofitBucket.setUrl(getUrlForRooms(version, baseUrl));
         Map<String, String> queryMap = new HashMap<>();
 
         queryMap.put("roomType", roomType);
@@ -151,9 +269,9 @@ public class ApiUtils {
         return retrofitBucket;
     }
 
-    public static RetrofitBucket getRetrofitBucketForAddParticipant(String baseUrl, String token, String user) {
+    public static RetrofitBucket getRetrofitBucketForAddParticipant(int version, String baseUrl, String token, String user) {
         RetrofitBucket retrofitBucket = new RetrofitBucket();
-        retrofitBucket.setUrl(baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + token + "/participants");
+        retrofitBucket.setUrl(getUrlForParticipants(version, baseUrl, token));
 
         Map<String, String> queryMap = new HashMap<>();
 
@@ -165,65 +283,26 @@ public class ApiUtils {
 
     }
 
-    public static RetrofitBucket getRetrofitBucketForAddGroupParticipant(String baseUrl, String token, String group) {
-        RetrofitBucket retrofitBucket = getRetrofitBucketForAddParticipant(baseUrl, token, group);
+    public static RetrofitBucket getRetrofitBucketForAddGroupParticipant(int version, String baseUrl, String token, String group) {
+        RetrofitBucket retrofitBucket = getRetrofitBucketForAddParticipant(version, baseUrl, token, group);
         retrofitBucket.getQueryMap().put("source", "groups");
         return retrofitBucket;
     }
 
-    public static RetrofitBucket getRetrofitBucketForAddMailParticipant(String baseUrl, String token, String mail) {
-        RetrofitBucket retrofitBucket = getRetrofitBucketForAddParticipant(baseUrl, token, mail);
+    public static RetrofitBucket getRetrofitBucketForAddMailParticipant(int version, String baseUrl, String token, String mail) {
+        RetrofitBucket retrofitBucket = getRetrofitBucketForAddParticipant(version, baseUrl, token, mail);
         retrofitBucket.getQueryMap().put("source", "emails");
         return retrofitBucket;
     }
 
-    public static String getUrlForRemoveSelfFromRoom(String baseUrl, String token) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + token + "/participants/self";
-    }
-
-    public static String getUrlForRoomVisibility(String baseUrl, String token) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + token + "/public";
-    }
-
-    public static String getUrlForCall(String baseUrl, String token) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/call/" + token;
-
-    }
-
+    /**
+     * @deprecated Method is only needed before Talk 4 which is from 2018 => todrop
+     */
+    @Deprecated
     public static String getUrlForCallPing(String baseUrl, String token) {
-        return getUrlForCall(baseUrl, token) + "/ping";
-    }
-
-    public static String getUrlForChat(String baseUrl, String token) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/chat/" + token;
-    }
-
-    public static String getUrlForExternalServerAuthBackend(String baseUrl) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/signaling/backend";
-    }
-
-    public static String getUrlForMentionSuggestions(String baseUrl, String token) {
-        return getUrlForChat(baseUrl, token) + "/mentions";
-    }
-
-    public static String getUrlForSignaling(String baseUrl, @Nullable String token) {
-        String signalingUrl = baseUrl + ocsApiVersion + spreedApiVersion + "/signaling";
-        if (token == null) {
-            return signalingUrl;
-        } else {
-            return signalingUrl + "/" + token;
-        }
-    }
-
-    public static String getUrlForModerators(String baseUrl, String roomToken) {
-        return getRoom(baseUrl, roomToken) + "/moderators";
-    }
-
-    public static String getUrlForSignalingSettings(String baseUrl) {
-        return getUrlForSignaling(baseUrl, null) + "/settings";
+        return getUrlForCall(1, baseUrl, token) + "/ping";
     }
 
-
     public static String getUrlForUserProfile(String baseUrl) {
         return baseUrl + ocsApiVersion + "/cloud/user";
     }
@@ -233,6 +312,7 @@ public class ApiUtils {
     }
 
     public static String getUrlForUserSettings(String baseUrl) {
+        // FIXME Introduce API version
         return baseUrl + ocsApiVersion + spreedApiVersion + "/settings/user";
     }
 
@@ -259,10 +339,6 @@ public class ApiUtils {
         return baseUrl + "/index.php/avatar/guest/" + Uri.encode(name) + "/" + avatarSize;
     }
 
-    public static String getUrlForPassword(String baseUrl, String token) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + token + "/password";
-    }
-
     public static String getCredentials(String username, String token) {
         if (TextUtils.isEmpty(username) && TextUtils.isEmpty(token)) {
             return null;
@@ -279,18 +355,10 @@ public class ApiUtils {
                 getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices";
     }
 
-    public static String getUrlForConversationFavorites(String baseUrl, String roomToken) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + roomToken + "/favorite";
-    }
-
     public static String getUrlForNotificationWithId(String baseUrl, String notificationId) {
         return baseUrl + ocsApiVersion + "/apps/notifications/api/v2/notifications/" + notificationId;
     }
 
-    public static String getUrlForReadOnlyState(String baseUrl, String roomToken) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + roomToken + "/read-only";
-    }
-
     public static String getUrlForSearchByNumber(String baseUrl) {
         return baseUrl + ocsApiVersion + "/cloud/users/search/by-phone";
     }
@@ -303,10 +371,6 @@ public class ApiUtils {
         return baseUrl + "/remote.php/dav/files/" + user + "/" + remotePath;
     }
 
-    public static String getUrlForMessageDeletion(String baseUrl, String token, String messageId) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/chat/" + token + "/" + messageId;
-    }
-
     public static String getUrlForTempAvatar(String baseUrl) {
         return baseUrl + ocsApiVersion + "/apps/spreed/temp-user-avatar";
     }

+ 23 - 0
app/src/main/java/com/nextcloud/talk/utils/NoSupportedApiException.kt

@@ -0,0 +1,23 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Joas Schilling
+ * Copyright (C) 2021 Joas Schilling <coding@schilljs.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils
+
+class NoSupportedApiException : RuntimeException("No supported API version found")

+ 5 - 1
app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java

@@ -53,6 +53,7 @@ public class DatabaseStorageModule implements StorageModule {
     private boolean lobbyValue;
 
     private String messageNotificationLevel;
+
     public DatabaseStorageModule(UserEntity conversationUser, String conversationToken) {
         NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
 
@@ -92,8 +93,11 @@ public class DatabaseStorageModule implements StorageModule {
                             intValue = 0;
                     }
 
+                    int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[] {4, 1});
+
                     ncApi.setNotificationLevel(ApiUtils.getCredentials(conversationUser.getUsername(), conversationUser.getToken()),
-                            ApiUtils.getUrlForSettingNotificationlevel(conversationUser.getBaseUrl(), conversationToken),
+                            ApiUtils.getUrlForRoomNotificationLevel(apiVersion, conversationUser.getBaseUrl(),
+                                                                    conversationToken),
                             intValue)
                             .subscribeOn(Schedulers.io())
                             .subscribe(new Observer<GenericOverall>() {

+ 3 - 1
app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java

@@ -88,12 +88,14 @@ public class WebSocketConnectionHelper {
     }
 
     HelloOverallWebSocketMessage getAssembledHelloModel(UserEntity userEntity, String ticket) {
+        int apiVersion = ApiUtils.getSignalingApiVersion(userEntity, new int[] {2, 1});
+
         HelloOverallWebSocketMessage helloOverallWebSocketMessage = new HelloOverallWebSocketMessage();
         helloOverallWebSocketMessage.setType("hello");
         HelloWebSocketMessage helloWebSocketMessage = new HelloWebSocketMessage();
         helloWebSocketMessage.setVersion("1.0");
         AuthWebSocketMessage authWebSocketMessage = new AuthWebSocketMessage();
-        authWebSocketMessage.setUrl(ApiUtils.getUrlForExternalServerAuthBackend(userEntity.getBaseUrl()));
+        authWebSocketMessage.setUrl(ApiUtils.getUrlForSignalingBackend(apiVersion, userEntity.getBaseUrl()));
         AuthParametersWebSocketMessage authParametersWebSocketMessage = new AuthParametersWebSocketMessage();
         authParametersWebSocketMessage.setTicket(ticket);
         authParametersWebSocketMessage.setUserid(userEntity.getUserId());