Pārlūkot izejas kodu

Add recording consent feature

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 1 gadu atpakaļ
vecāks
revīzija
bfbc352448

+ 75 - 4
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<RoomOverall> {
+                        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()
     }
 

+ 10 - 2
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -245,9 +245,11 @@ public interface NcApi {
 
     @FormUrlEncoded
     @POST
-    Observable<GenericOverall> joinCall(@Nullable @Header("Authorization") String authorization, @Url String url,
+    Observable<GenericOverall> 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<ReminderOverall> setReminder(@Header("Authorization") String authorization,
                                             @Url String url,
                                             @Field("timestamp") int timestamp);
+
+    @FormUrlEncoded
+    @PUT
+    Observable<GenericOverall> setRecordingConsent(@Header("Authorization") String authorization,
+                                                    @Url String url,
+                                                    @Field("recordingConsent") int recordingConsent);
 }

+ 87 - 2
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<GenericOverall> {
+                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
     }
 
     /**

+ 4 - 1
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'

+ 4 - 0
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";
+    }
 }

+ 24 - 0
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
 }

+ 6 - 1
app/src/main/res/layout/activity_conversation_info.xml

@@ -226,6 +226,12 @@
                 </LinearLayout>
             </LinearLayout>
 
+            <include
+                android:id="@+id/recording_consent_view"
+                layout="@layout/item_recording_consent"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/standard_quarter_margin" />
 
             <LinearLayout
                 android:id="@+id/conversation_settings"
@@ -261,7 +267,6 @@
                         android:popupTheme="@style/ThemeOverlay.AppTheme.PopupMenu"
                         android:text="" />
 
-
                 </com.google.android.material.textfield.TextInputLayout>
 
                 <com.google.android.material.textview.MaterialTextView

+ 89 - 0
app/src/main/res/layout/item_recording_consent.xml

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ 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/>.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/recording_consent_settings"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/recording_consent_settings_category"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/standard_margin"
+        android:layout_marginEnd="@dimen/standard_margin"
+        android:paddingTop="@dimen/standard_padding"
+        android:paddingBottom="@dimen/standard_half_padding"
+        android:text="@string/recording_settings_title"
+        android:textSize="@dimen/headline_text_size"
+        android:textStyle="bold" />
+
+    <LinearLayout
+        android:id="@+id/recording_consent_for_conversation"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="?android:attr/selectableItemBackground"
+        android:orientation="horizontal"
+        android:paddingStart="@dimen/standard_margin"
+        android:paddingTop="@dimen/standard_margin"
+        android:paddingEnd="@dimen/standard_margin"
+        android:paddingBottom="@dimen/standard_half_margin">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <com.google.android.material.textview.MaterialTextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/recording_consent_for_conversation_title"
+                android:textSize="@dimen/headline_text_size" />
+
+            <com.google.android.material.textview.MaterialTextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:hint="@string/recording_consent_for_conversation_description"
+                android:textSize="@dimen/supporting_text_text_size" />
+
+        </LinearLayout>
+
+        <com.google.android.material.materialswitch.MaterialSwitch
+            android:id="@+id/recording_consent_for_conversation_switch"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginStart="@dimen/standard_margin"
+            android:clickable="false" />
+
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/recording_consent_all"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingStart="@dimen/standard_margin"
+        android:paddingTop="@dimen/standard_margin"
+        android:paddingEnd="@dimen/standard_margin"
+        android:paddingBottom="@dimen/standard_half_margin"
+        android:text="@string/recording_consent_all" />
+
+</LinearLayout>

+ 6 - 1
app/src/main/res/values/strings.xml

@@ -606,6 +606,12 @@ How to translate with transifex:
     <string name="record_cancel_start">Cancel recording start</string>
     <string name="record_stopping">Stopping recording …</string>
     <string name="record_failed_info">The recording failed. Please contact your administrator.</string>
+    <string name="recording_consent_title">The call might be recorded.</string>
+    <string name="recording_consent_description">The recording might include your voice, video from camera, and screen share. Your consent is required before joining the call. Do you consent?</string>
+    <string name="recording_settings_title">Recording</string>
+    <string name="recording_consent_for_conversation_title">Recording consent</string>
+    <string name="recording_consent_for_conversation_description">Require recording consent before joining call in this conversation</string>
+    <string name="recording_consent_all">Recording consent is required for all calls</string>
 
     <!-- Shared items -->
     <string name="nc_shared_items">Shared items</string>
@@ -708,5 +714,4 @@ How to translate with transifex:
     <string name="audio_call">Audio Call</string>
     <string name="started_a_call">started a call</string>
     <string name="nc_settings_phone_book_integration_phone_number_dialog_429">Error 429 Too Many Requests</string>
-
 </resources>