mirror of
https://github.com/nextcloud/talk-android.git
synced 2024-11-22 13:05:31 +03:00
Add recording consent feature
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
parent
951f80315e
commit
bfbc352448
9 changed files with 305 additions and 11 deletions
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
app/src/main/res/layout/item_recording_consent.xml
Normal file
89
app/src/main/res/layout/item_recording_consent.xml
Normal file
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue