Simple menu to select sound device

This commit is contained in:
Valere 2020-06-15 15:46:54 +02:00
parent 248b9ff1e1
commit 0f625c27a1
13 changed files with 242 additions and 11 deletions

View File

@ -24,6 +24,7 @@ import dagger.Component
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.call.CallControlsBottomSheet
import im.vector.riotx.features.call.VectorCallActivity import im.vector.riotx.features.call.VectorCallActivity
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
@ -148,6 +149,7 @@ interface ScreenComponent {
fun inject(bottomSheet: BootstrapBottomSheet) fun inject(bottomSheet: BootstrapBottomSheet)
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet) fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
fun inject(bottomSheet: RoomWidgetsBottomSheet) fun inject(bottomSheet: RoomWidgetsBottomSheet)
fun inject(callControlsBottomSheet: CallControlsBottomSheet)
/* ========================================================================================== /* ==========================================================================================
* Others * Others

View File

@ -34,6 +34,7 @@ import com.airbnb.mvrx.MvRxViewId
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.jakewharton.rxbinding3.view.clicks
import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
@ -41,6 +42,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit
/** /**
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment) * Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
@ -169,6 +171,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
return this return this
} }
/* ==========================================================================================
* Views
* ========================================================================================== */
protected fun View.debouncedClicks(onClicked: () -> Unit) {
clicks()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onClicked() }
.disposeOnDestroyView()
}
/* ========================================================================================== /* ==========================================================================================
* ViewEvents * ViewEvents
* ========================================================================================== */ * ========================================================================================== */

View File

@ -26,6 +26,11 @@ class CallAudioManager(
val applicationContext: Context val applicationContext: Context
) { ) {
enum class SoundDevice {
PHONE,
SPEAKER
}
private val audioManager: AudioManager = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager private val audioManager: AudioManager = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var savedIsSpeakerPhoneOn = false private var savedIsSpeakerPhoneOn = false
@ -69,6 +74,8 @@ class CallAudioManager(
// TODO check if there are headsets? // TODO check if there are headsets?
if (mxCall.isVideoCall) { if (mxCall.isVideoCall) {
setSpeakerphoneOn(true) setSpeakerphoneOn(true)
} else {
setSpeakerphoneOn(false)
} }
} }
@ -84,6 +91,21 @@ class CallAudioManager(
audioManager.abandonAudioFocus(audioFocusChangeListener) audioManager.abandonAudioFocus(audioFocusChangeListener)
} }
fun getCurrentSoundDevice() : SoundDevice {
if (audioManager.isSpeakerphoneOn) {
return SoundDevice.SPEAKER
} else {
return SoundDevice.PHONE
}
}
fun setCurrentSoundDevice(device: SoundDevice) {
when (device) {
SoundDevice.PHONE -> setSpeakerphoneOn(false)
SoundDevice.SPEAKER -> setSpeakerphoneOn(true)
}
}
/** Sets the speaker phone mode. */ /** Sets the speaker phone mode. */
private fun setSpeakerphoneOn(on: Boolean) { private fun setSpeakerphoneOn(on: Boolean) {
Timber.v("## VOIP: AudioManager setSpeakerphoneOn $on") Timber.v("## VOIP: AudioManager setSpeakerphoneOn $on")

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.call
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import kotlinx.android.synthetic.main.bottom_sheet_call_controls.*
class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun getLayoutResId() = R.layout.bottom_sheet_call_controls
private val callViewModel: VectorCallViewModel by activityViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callViewModel.subscribe(this) {
renderState(it)
}
callControlsSoundDevice.clickableView.debouncedClicks {
val soundDevices = listOf(
getString(R.string.sound_device_phone),
getString(R.string.sound_device_speaker)
)
AlertDialog.Builder(requireContext())
.setItems(soundDevices.toTypedArray()) { d, n ->
d.cancel()
when (soundDevices[n]) {
getString(R.string.sound_device_phone) -> {
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE))
}
getString(R.string.sound_device_speaker) -> {
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER))
}
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
// override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// return super.onCreateDialog(savedInstanceState).apply {
// window?.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
// window?.decorView?.systemUiVisibility =
// View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
// View.SYSTEM_UI_FLAG_FULLSCREEN or
// View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
// }
// }
private fun renderState(state: VectorCallViewState) {
callControlsSoundDevice.title = getString(R.string.call_select_sound_device)
callControlsSoundDevice.subTitle = when (state.soundDevice) {
CallAudioManager.SoundDevice.PHONE -> getString(R.string.sound_device_phone)
CallAudioManager.SoundDevice.SPEAKER -> getString(R.string.sound_device_speaker)
}
}
}

View File

@ -22,7 +22,6 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -89,6 +88,11 @@ class CallControlsView @JvmOverloads constructor(
interactionListener?.returnToChat() interactionListener?.returnToChat()
} }
@OnClick(R.id.iv_more)
fun moreControlOption() {
interactionListener?.didTapMore()
}
fun updateForState(state: VectorCallViewState) { fun updateForState(state: VectorCallViewState) {
val callState = state.callState.invoke() val callState = state.callState.invoke()
muteIcon.setImageResource(if (state.isAudioMuted) R.drawable.ic_microphone_off else R.drawable.ic_microphone_on) muteIcon.setImageResource(if (state.isAudioMuted) R.drawable.ic_microphone_off else R.drawable.ic_microphone_on)
@ -113,7 +117,7 @@ class CallControlsView @JvmOverloads constructor(
CallState.CONNECTED -> { CallState.CONNECTED -> {
ringingControls.isVisible = false ringingControls.isVisible = false
connectedControls.isVisible = true connectedControls.isVisible = true
iv_video_toggle.isInvisible = !state.isVideoCall iv_video_toggle.isVisible = state.isVideoCall
} }
CallState.TERMINATED, CallState.TERMINATED,
null -> { null -> {
@ -130,5 +134,6 @@ class CallControlsView @JvmOverloads constructor(
fun didTapToggleMute() fun didTapToggleMute()
fun didTapToggleVideo() fun didTapToggleVideo()
fun returnToChat() fun returnToChat()
fun didTapMore()
} }
} }

View File

@ -26,11 +26,14 @@ import android.os.Parcelable
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import butterknife.BindView import butterknife.BindView
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import com.jakewharton.rxbinding3.view.clicks
import im.vector.matrix.android.api.session.call.CallState import im.vector.matrix.android.api.session.call.CallState
import im.vector.matrix.android.api.session.call.EglUtils import im.vector.matrix.android.api.session.call.EglUtils
import im.vector.matrix.android.api.session.call.MxCallDetail import im.vector.matrix.android.api.session.call.MxCallDetail
@ -46,10 +49,12 @@ import im.vector.riotx.features.home.AvatarRenderer
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_call.* import kotlinx.android.synthetic.main.activity_call.*
import kotlinx.android.synthetic.main.fragment_attachments_preview.*
import org.webrtc.EglBase import org.webrtc.EglBase
import org.webrtc.RendererCommon import org.webrtc.RendererCommon
import org.webrtc.SurfaceViewRenderer import org.webrtc.SurfaceViewRenderer
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@Parcelize @Parcelize
@ -91,6 +96,8 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
private var rootEglBase: EglBase? = null private var rootEglBase: EglBase? = null
var systemUiVisibility = false
override fun doBeforeSetContentView() { override fun doBeforeSetContentView() {
// Set window styles for fullscreen-window size. Needs to be done before adding content. // Set window styles for fullscreen-window size. Needs to be done before adding content.
requestWindowFeature(Window.FEATURE_NO_TITLE) requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -108,14 +115,64 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
) )
} }
window.decorView.systemUiVisibility = hideSystemUI()
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
setContentView(R.layout.activity_call) setContentView(R.layout.activity_call)
} }
private fun hideSystemUI() {
systemUiVisibility = false
// Enables regular immersive mode.
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// Hide the nav bar and status bar
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
// Shows the system bars by removing all the flags
// except for the ones that make the content appear under the system bars.
private fun showSystemUI() {
systemUiVisibility = true
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
}
private fun toggleUiSystemVisibility() {
if (systemUiVisibility) {
hideSystemUI()
} else {
showSystemUI()
}
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
// Rehide when bottom sheet is dismissed
if (hasFocus) {
hideSystemUI()
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// This will need to be refined
ViewCompat.setOnApplyWindowInsetsListener(constraintLayout) { v, insets ->
v.updatePadding(bottom = if (systemUiVisibility) insets.systemWindowInsetBottom else 0)
insets
}
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// // window.navigationBarColor = ContextCompat.getColor(this, R.color.riotx_background_light)
// // }
// for content intent when screen is locked
// window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); // window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
// window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@ -125,6 +182,12 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
finish() finish()
} }
constraintLayout.clicks()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { toggleUiSystemVisibility() }
.disposeOnDestroy()
if (isFirstCreation()) { if (isFirstCreation()) {
// Reduce priority of notification as the activity is on screen // Reduce priority of notification as the activity is on screen
CallService.onPendingCall( CallService.onPendingCall(
@ -342,4 +405,8 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
// TODO, what if the room is not in backstack?? // TODO, what if the room is not in backstack??
finish() finish()
} }
override fun didTapMore() {
CallControlsBottomSheet().show(supportFragmentManager, "Controls")
}
} }

View File

@ -41,6 +41,7 @@ data class VectorCallViewState(
val isVideoCall: Boolean, val isVideoCall: Boolean,
val isAudioMuted: Boolean = false, val isAudioMuted: Boolean = false,
val isVideoEnabled: Boolean = true, val isVideoEnabled: Boolean = true,
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized, val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
val callState: Async<CallState> = Uninitialized val callState: Async<CallState> = Uninitialized
) : MvRxState ) : MvRxState
@ -51,6 +52,7 @@ sealed class VectorCallViewActions : VectorViewModelAction {
object DeclineCall : VectorCallViewActions() object DeclineCall : VectorCallViewActions()
object ToggleMute : VectorCallViewActions() object ToggleMute : VectorCallViewActions()
object ToggleVideo : VectorCallViewActions() object ToggleVideo : VectorCallViewActions()
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
} }
sealed class VectorCallViewEvents : VectorViewEvents { sealed class VectorCallViewEvents : VectorViewEvents {
@ -86,6 +88,7 @@ class VectorCallViewModel @AssistedInject constructor(
autoReplyIfNeeded = args.autoAccept autoReplyIfNeeded = args.autoAccept
initialState.callId?.let { initialState.callId?.let {
session.callSignalingService().getCallWithId(it)?.let { mxCall -> session.callSignalingService().getCallWithId(it)?.let { mxCall ->
this.call = mxCall this.call = mxCall
mxCall.otherUserId mxCall.otherUserId
@ -96,7 +99,8 @@ class VectorCallViewModel @AssistedInject constructor(
copy( copy(
isVideoCall = mxCall.isVideoCall, isVideoCall = mxCall.isVideoCall,
callState = Success(mxCall.state), callState = Success(mxCall.state),
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized,
soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice()
) )
} }
} }
@ -111,20 +115,20 @@ class VectorCallViewModel @AssistedInject constructor(
override fun handle(action: VectorCallViewActions) = withState { override fun handle(action: VectorCallViewActions) = withState {
when (action) { when (action) {
VectorCallViewActions.EndCall -> webRtcPeerConnectionManager.endCall() VectorCallViewActions.EndCall -> webRtcPeerConnectionManager.endCall()
VectorCallViewActions.AcceptCall -> { VectorCallViewActions.AcceptCall -> {
setState { setState {
copy(callState = Loading()) copy(callState = Loading())
} }
webRtcPeerConnectionManager.acceptIncomingCall() webRtcPeerConnectionManager.acceptIncomingCall()
} }
VectorCallViewActions.DeclineCall -> { VectorCallViewActions.DeclineCall -> {
setState { setState {
copy(callState = Loading()) copy(callState = Loading())
} }
webRtcPeerConnectionManager.endCall() webRtcPeerConnectionManager.endCall()
} }
VectorCallViewActions.ToggleMute -> { VectorCallViewActions.ToggleMute -> {
withState { withState {
val muted = it.isAudioMuted val muted = it.isAudioMuted
webRtcPeerConnectionManager.muteCall(!muted) webRtcPeerConnectionManager.muteCall(!muted)
@ -133,7 +137,7 @@ class VectorCallViewModel @AssistedInject constructor(
} }
} }
} }
VectorCallViewActions.ToggleVideo -> { VectorCallViewActions.ToggleVideo -> {
withState { withState {
if (it.isVideoCall) { if (it.isVideoCall) {
val videoEnabled = it.isVideoEnabled val videoEnabled = it.isVideoEnabled
@ -144,6 +148,14 @@ class VectorCallViewModel @AssistedInject constructor(
} }
} }
} }
is VectorCallViewActions.ChangeAudioDevice -> {
webRtcPeerConnectionManager.audioManager.setCurrentSoundDevice(action.device)
setState {
copy(
soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice()
)
}
}
}.exhaustive }.exhaustive
} }

View File

@ -5,6 +5,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/constraintLayout"
android:background="?riotx_background" android:background="?riotx_background"
tools:ignore="MergeRootFrame"> tools:ignore="MergeRootFrame">

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/callControlsWrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<im.vector.riotx.core.ui.views.BottomSheetActionButton
android:id="@+id/callControlsSoundDevice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:actionTitle="Select sound device"
tools:actionDescription="Speaker"
app:leftIcon="@drawable/ic_call_speaker_default"
app:tint="?attr/riotx_text_primary" />
</LinearLayout>

View File

@ -123,11 +123,13 @@
android:layout_width="44dp" android:layout_width="44dp"
android:layout_height="44dp" android:layout_height="44dp"
android:layout_marginBottom="32dp" android:layout_marginBottom="32dp"
android:background="@drawable/oval_positive"
android:backgroundTint="?attr/riotx_background"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:padding="8dp" android:padding="8dp"
android:src="@drawable/ic_more_vertical" android:src="@drawable/ic_more_vertical"
android:tint="?attr/riotx_background" android:tint="?attr/riotx_text_primary"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow <androidx.constraintlayout.helper.widget.Flow

View File

@ -127,6 +127,8 @@
<im.vector.riotx.features.home.room.detail.widget.RoomWidgetsBannerView <im.vector.riotx.features.home.room.detail.widget.RoomWidgetsBannerView
android:id="@+id/roomWidgetsBannerView" android:id="@+id/roomWidgetsBannerView"
android:visibility="gone"
tools:visibility="visible"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"

View File

@ -4,6 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?colorPrimary" android:background="?colorPrimary"
android:foreground="?attr/selectableItemBackground"
tools:parentTag="android.widget.RelativeLayout"> tools:parentTag="android.widget.RelativeLayout">
<TextView <TextView
@ -31,6 +32,8 @@
android:layout_alignBottom="@+id/activeCallInfo" android:layout_alignBottom="@+id/activeCallInfo"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:clickable="false"
android:focusable="false"
android:textColor="@color/white" android:textColor="@color/white"
android:gravity="center" android:gravity="center"
android:textAllCaps="true" android:textAllCaps="true"

View File

@ -210,6 +210,10 @@
<string name="call_failed_no_ice_description">Please ask the administrator of your homeserver (%1$s) to configure a TURN server in order for calls to work reliably.\n\nAlternatively, you can try to use the public server at %2$s, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings."</string> <string name="call_failed_no_ice_description">Please ask the administrator of your homeserver (%1$s) to configure a TURN server in order for calls to work reliably.\n\nAlternatively, you can try to use the public server at %2$s, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings."</string>
<string name="call_failed_no_ice_use_alt">Try using %s</string> <string name="call_failed_no_ice_use_alt">Try using %s</string>
<string name="call_failed_dont_ask_again">Do not ask me again</string> <string name="call_failed_dont_ask_again">Do not ask me again</string>
<string name="call_select_sound_device">Select Sound Device</string>
<string name="sound_device_phone">Phone</string>
<string name="sound_device_speaker">Speaker</string>
<string name="option_send_files">Send files</string> <string name="option_send_files">Send files</string>
<string name="option_send_sticker">Send sticker</string> <string name="option_send_sticker">Send sticker</string>