diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 268e42eae..6156da973 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -130,6 +130,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM +import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isCallRecordingAvailable import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew @@ -376,6 +377,8 @@ class CallActivity : CallBaseActivity() { AudioFormat.ENCODING_PCM_16BIT ) + private var recordingConsentGiven = false + @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { Log.d(TAG, "onCreate") @@ -496,11 +499,72 @@ class CallActivity : CallBaseActivity() { callParticipants = HashMap() participantDisplayItems = HashMap() initViews() - if (!isConnectionEstablished) { - initiateCall() - } updateSelfVideoViewPosition() reactionAnimator = ReactionAnimator(context, binding!!.reactionAnimationWrapper, viewThemeUtils) + + checkRecordingConsentAndInitiateCall() + } + + private fun checkRecordingConsentAndInitiateCall() { + fun askForRecordingConsent() { + val materialAlertDialogBuilder = MaterialAlertDialogBuilder(this) + .setTitle(R.string.recording_consent_title) + .setMessage(R.string.recording_consent_description) + .setCancelable(false) + .setPositiveButton(R.string.nc_yes) { _, _ -> + recordingConsentGiven = true + initiateCall() + } + .setNegativeButton(R.string.nc_no) { _, _ -> + recordingConsentGiven = false + hangup(true) + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, materialAlertDialogBuilder) + val dialog = materialAlertDialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + + when (CapabilitiesUtilNew.getRecordingConsentType(conversationUser)) { + CapabilitiesUtilNew.RECORDING_CONSENT_NOT_REQUIRED -> initiateCall() + CapabilitiesUtilNew.RECORDING_CONSENT_REQUIRED -> askForRecordingConsent() + CapabilitiesUtilNew.RECORDING_CONSENT_DEPEND_ON_CONVERSATION -> { + val getRoomApiVersion = ApiUtils.getConversationApiVersion( + conversationUser, + intArrayOf(ApiUtils.APIv4, 1) + ) + ncApi!!.getRoom(credentials, ApiUtils.getUrlForRoom(getRoomApiVersion, baseUrl, roomToken)) + .retry(API_RETRIES) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(roomOverall: RoomOverall) { + val conversation = roomOverall.ocs!!.data + if (conversation?.recordingConsentRequired == 1) { + askForRecordingConsent() + } else { + initiateCall() + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to get room", e) + Snackbar.make(binding!!.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + override fun onComplete() { + // unused atm + } + }) + } + } } override fun onResume() { @@ -1660,7 +1724,8 @@ class CallActivity : CallBaseActivity() { credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken), inCallFlag, - isCallWithoutNotification + isCallWithoutNotification, + recordingConsentGiven ) .subscribeOn(Schedulers.io()) .retry(API_RETRIES) @@ -1676,6 +1741,8 @@ class CallActivity : CallBaseActivity() { override fun onError(e: Throwable) { Log.e(TAG, "Failed to join call", e) + Snackbar.make(binding!!.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + hangup(true) } override fun onComplete() { @@ -1804,6 +1871,10 @@ class CallActivity : CallBaseActivity() { } private fun initiateCall() { + if (isConnectionEstablished) { + Log.d(TAG, "connection already established") + return + } checkDevicePermissions() } diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 6dad5fefc..7e8f3451a 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -245,9 +245,11 @@ public interface NcApi { @FormUrlEncoded @POST - Observable joinCall(@Nullable @Header("Authorization") String authorization, @Url String url, + Observable joinCall(@Nullable @Header("Authorization") String authorization, + @Url String url, @Field("flags") Integer inCall, - @Field("silent") Boolean callWithoutNotification); + @Field("silent") Boolean callWithoutNotification, + @Nullable @Field("recordingConsent") Boolean recordingConsent); /* Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken @@ -686,4 +688,10 @@ public interface NcApi { Observable setReminder(@Header("Authorization") String authorization, @Url String url, @Field("timestamp") int timestamp); + + @FormUrlEncoded + @PUT + Observable setRecordingConsent(@Header("Authorization") String authorization, + @Url String url, + @Field("recordingConsent") int recordingConsent); } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index 3d0b3f1f9..42c5ef67e 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -247,7 +247,8 @@ class ConversationInfoActivity : binding.notificationSettingsView.callNotificationsSwitch, binding.notificationSettingsView.importantConversationSwitch, binding.guestAccessView.allowGuestsSwitch, - binding.guestAccessView.passwordProtectionSwitch + binding.guestAccessView.passwordProtectionSwitch, + binding.recordingConsentView.recordingConsentForConversationSwitch ).forEach(viewThemeUtils.talk::colorSwitch) } } @@ -259,6 +260,7 @@ class ConversationInfoActivity : binding.webinarInfoView.webinarSettingsCategory, binding.guestAccessView.guestAccessSettingsCategory, binding.sharedItemsTitle, + binding.recordingConsentView.recordingConsentSettingsCategory, binding.conversationSettingsTitle, binding.participantsListCategory ) @@ -707,6 +709,7 @@ class ConversationInfoActivity : loadConversationAvatar() adjustNotificationLevelUI() + initRecordingConsentOption() initExpiringMessageOption() binding.let { @@ -738,6 +741,86 @@ class ConversationInfoActivity : }) } + private fun initRecordingConsentOption() { + fun hide() { + binding.recordingConsentView.recordingConsentSettingsCategory.visibility = GONE + binding.recordingConsentView.recordingConsentForConversation.visibility = GONE + binding.recordingConsentView.recordingConsentAll.visibility = GONE + } + + fun showAlwaysRequiredInfo() { + binding.recordingConsentView.recordingConsentForConversation.visibility = GONE + binding.recordingConsentView.recordingConsentAll.visibility = VISIBLE + } + + fun showSwitch() { + binding.recordingConsentView.recordingConsentForConversation.visibility = VISIBLE + binding.recordingConsentView.recordingConsentAll.visibility = GONE + + if (conversation!!.hasCall) { + binding.recordingConsentView.recordingConsentForConversation.isEnabled = false + binding.recordingConsentView.recordingConsentForConversation.alpha = LOW_EMPHASIS_OPACITY + } else { + binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked = + conversation!!.recordingConsentRequired == RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION + + binding.recordingConsentView.recordingConsentForConversation.setOnClickListener { + binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked = + !binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked + submitRecordingConsentChanges() + } + } + } + + if (conversation!!.isParticipantOwnerOrModerator && + !ConversationUtils.isNoteToSelfConversation(ConversationModel.mapToConversationModel(conversation!!)) + ) { + when (CapabilitiesUtilNew.getRecordingConsentType(conversationUser)) { + CapabilitiesUtilNew.RECORDING_CONSENT_NOT_REQUIRED -> hide() + CapabilitiesUtilNew.RECORDING_CONSENT_REQUIRED -> showAlwaysRequiredInfo() + CapabilitiesUtilNew.RECORDING_CONSENT_DEPEND_ON_CONVERSATION -> showSwitch() + } + } else { + hide() + } + } + + private fun submitRecordingConsentChanges() { + val state = if (binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked) { + RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION + } else { + RECORDING_CONSENT_NOT_REQUIRED_FOR_CONVERSATION + } + + val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1)) + + ncApi.setRecordingConsent( + ApiUtils.getCredentials(conversationUser.username, conversationUser.token), + ApiUtils.getUrlForRecordingConsent(apiVersion, conversationUser.baseUrl, conversation!!.token), + state + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onComplete() { + // unused atm + } + + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(t: GenericOverall) { + // unused atm + } + + override fun onError(e: Throwable) { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Log.e(TAG, "Error when setting recording consent option for conversation", e) + } + }) + } + private fun initExpiringMessageOption() { if (conversation!!.isParticipantOwnerOrModerator && !ConversationUtils.isNoteToSelfConversation(ConversationModel.mapToConversationModel(conversation!!)) && @@ -1227,11 +1310,13 @@ class ConversationInfoActivity : } companion object { - private const val TAG = "ConversationInfo" + private val TAG = ConversationInfoActivity::class.java.simpleName private const val NOTIFICATION_LEVEL_ALWAYS: Int = 1 private const val NOTIFICATION_LEVEL_MENTION: Int = 2 private const val NOTIFICATION_LEVEL_NEVER: Int = 3 private const val LOW_EMPHASIS_OPACITY: Float = 0.38f + private const val RECORDING_CONSENT_NOT_REQUIRED_FOR_CONVERSATION: Int = 0 + private const val RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION: Int = 1 } /** diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt index 057e5d724..e3dd2ea92 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt @@ -157,7 +157,10 @@ data class Conversation( var hasCustomAvatar: Boolean? = null, @JsonField(name = ["callStartTime"]) - var callStartTime: Long? = null + var callStartTime: Long? = null, + + @JsonField(name = ["recordingConsent"]) + var recordingConsentRequired: Int = 0 ) : Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index f08b31e36..9cac57436 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -536,4 +536,8 @@ public class ApiUtils { String url = ApiUtils.getUrlForChatMessage(version, user.getBaseUrl(), roomToken, messageId); return url + "/reminder"; } + + public static String getUrlForRecordingConsent(int version, String baseUrl, String token) { + return getUrlForRoom(version, baseUrl, token) + "/recording-consent"; + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt index 5816fd356..babf6bed6 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt @@ -250,5 +250,29 @@ object CapabilitiesUtilNew { return false } + fun getRecordingConsentType(user: User?): Int { + if (user?.capabilities != null) { + val capabilities = user.capabilities + if ( + capabilities?.spreedCapability?.config?.containsKey("call") == true && + capabilities.spreedCapability!!.config!!["call"] != null && + capabilities.spreedCapability!!.config!!["call"]!!.containsKey("recording-consent") + ) { + return when ( + capabilities.spreedCapability!!.config!!["call"]!!["recording-consent"].toString() + .toInt() + ) { + 1 -> RECORDING_CONSENT_REQUIRED + 2 -> RECORDING_CONSENT_DEPEND_ON_CONVERSATION + else -> RECORDING_CONSENT_NOT_REQUIRED + } + } + } + return RECORDING_CONSENT_NOT_REQUIRED + } + const val DEFAULT_CHAT_SIZE = 1000 + const val RECORDING_CONSENT_NOT_REQUIRED = 0 + const val RECORDING_CONSENT_REQUIRED = 1 + const val RECORDING_CONSENT_DEPEND_ON_CONVERSATION = 2 } diff --git a/app/src/main/res/layout/activity_conversation_info.xml b/app/src/main/res/layout/activity_conversation_info.xml index f92306cca..bcb02b2aa 100644 --- a/app/src/main/res/layout/activity_conversation_info.xml +++ b/app/src/main/res/layout/activity_conversation_info.xml @@ -226,6 +226,12 @@ + - + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa34cd5e5..1514e25a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -606,6 +606,12 @@ How to translate with transifex: Cancel recording start Stopping recording … The recording failed. Please contact your administrator. + The call might be recorded. + The recording might include your voice, video from camera, and screen share. Your consent is required before joining the call. Do you consent? + Recording + Recording consent + Require recording consent before joining call in this conversation + Recording consent is required for all calls Shared items @@ -708,5 +714,4 @@ How to translate with transifex: Audio Call started a call Error 429 Too Many Requests -