mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-01-31 11:24:58 +01:00
Support toggle front/back camera
This commit is contained in:
parent
77a01f0cd4
commit
b27eead016
@ -19,6 +19,7 @@ package im.vector.riotx.features.call
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
@ -41,6 +42,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
callViewModel.handle(VectorCallViewActions.SwitchSoundDevice)
|
||||
}
|
||||
|
||||
callControlsSwitchCamera.clickableView.debouncedClicks {
|
||||
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
callViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
|
||||
@ -55,19 +61,19 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
private fun showSoundDeviceChooser(available: List<CallAudioManager.SoundDevice>, current: CallAudioManager.SoundDevice) {
|
||||
val soundDevices = available.map {
|
||||
when (it) {
|
||||
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> span {
|
||||
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> span {
|
||||
text = getString(R.string.sound_device_wireless_headset)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.SoundDevice.PHONE -> span {
|
||||
CallAudioManager.SoundDevice.PHONE -> span {
|
||||
text = getString(R.string.sound_device_phone)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.SoundDevice.SPEAKER -> span {
|
||||
CallAudioManager.SoundDevice.SPEAKER -> span {
|
||||
text = getString(R.string.sound_device_speaker)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.SoundDevice.HEADSET -> span {
|
||||
CallAudioManager.SoundDevice.HEADSET -> span {
|
||||
text = getString(R.string.sound_device_headset)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
@ -77,13 +83,13 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
.setItems(soundDevices.toTypedArray()) { d, n ->
|
||||
d.cancel()
|
||||
when (soundDevices[n].toString()) {
|
||||
getString(R.string.sound_device_phone) -> {
|
||||
getString(R.string.sound_device_phone) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE))
|
||||
}
|
||||
getString(R.string.sound_device_speaker) -> {
|
||||
getString(R.string.sound_device_speaker) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER))
|
||||
}
|
||||
getString(R.string.sound_device_headset) -> {
|
||||
getString(R.string.sound_device_headset) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.HEADSET))
|
||||
}
|
||||
getString(R.string.sound_device_wireless_headset) -> {
|
||||
@ -95,23 +101,16 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
.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)
|
||||
CallAudioManager.SoundDevice.HEADSET -> getString(R.string.sound_device_headset)
|
||||
CallAudioManager.SoundDevice.PHONE -> getString(R.string.sound_device_phone)
|
||||
CallAudioManager.SoundDevice.SPEAKER -> getString(R.string.sound_device_speaker)
|
||||
CallAudioManager.SoundDevice.HEADSET -> getString(R.string.sound_device_headset)
|
||||
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset)
|
||||
}
|
||||
|
||||
callControlsSwitchCamera.isVisible = state.canSwitchCamera
|
||||
callControlsSwitchCamera.subTitle = getString(if (state.isFrontCamera) R.string.call_camera_front else R.string.call_camera_back)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
enum class CameraType {
|
||||
FRONT,
|
||||
BACK
|
||||
}
|
||||
|
||||
data class CameraProxy(
|
||||
val name: String,
|
||||
val type: CameraType
|
||||
)
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.riotx.features.call
|
||||
|
||||
// import im.vector.riotx.features.call.service.CallHeadsUpService
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -161,13 +160,8 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
|
||||
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_KEEP_SCREEN_ON);
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
if (intent.hasExtra(MvRx.KEY_ARG)) {
|
||||
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
|
||||
@ -323,6 +317,10 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
|
||||
|
||||
peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer,
|
||||
intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() })
|
||||
|
||||
pipRenderer.setOnClickListener {
|
||||
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
|
@ -48,6 +48,8 @@ data class VectorCallViewState(
|
||||
val isAudioMuted: Boolean = false,
|
||||
val isVideoEnabled: Boolean = true,
|
||||
val isVideoCaptureInError: Boolean = false,
|
||||
val isFrontCamera: Boolean = true,
|
||||
val canSwitchCamera: Boolean = true,
|
||||
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
|
||||
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
|
||||
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
|
||||
@ -62,7 +64,8 @@ sealed class VectorCallViewActions : VectorViewModelAction {
|
||||
object ToggleVideo : VectorCallViewActions()
|
||||
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
|
||||
object SwitchSoundDevice : VectorCallViewActions()
|
||||
object HeadSetButtonPressed: VectorCallViewActions()
|
||||
object HeadSetButtonPressed : VectorCallViewActions()
|
||||
object ToggleCamera : VectorCallViewActions()
|
||||
}
|
||||
|
||||
sealed class VectorCallViewEvents : VectorViewEvents {
|
||||
@ -140,6 +143,15 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCameraChange(mgr: WebRtcPeerConnectionManager) {
|
||||
setState {
|
||||
copy(
|
||||
canSwitchCamera = mgr.canSwitchCamera(),
|
||||
isFrontCamera = mgr.currentCameraType() == CameraType.FRONT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
@ -160,7 +172,9 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
callState = Success(mxCall.state),
|
||||
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized,
|
||||
soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice(),
|
||||
availableSoundDevices = webRtcPeerConnectionManager.audioManager.getAvailableSoundDevices()
|
||||
availableSoundDevices = webRtcPeerConnectionManager.audioManager.getAvailableSoundDevices(),
|
||||
isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT,
|
||||
canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera()
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
@ -236,6 +250,9 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
}
|
||||
Unit
|
||||
}
|
||||
VectorCallViewActions.ToggleCamera -> {
|
||||
webRtcPeerConnectionManager.switchCamera()
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ import org.webrtc.AudioSource
|
||||
import org.webrtc.AudioTrack
|
||||
import org.webrtc.Camera1Enumerator
|
||||
import org.webrtc.Camera2Enumerator
|
||||
import org.webrtc.CameraVideoCapturer
|
||||
import org.webrtc.DataChannel
|
||||
import org.webrtc.DefaultVideoDecoderFactory
|
||||
import org.webrtc.DefaultVideoEncoderFactory
|
||||
@ -54,7 +55,6 @@ import org.webrtc.RtpReceiver
|
||||
import org.webrtc.SessionDescription
|
||||
import org.webrtc.SurfaceTextureHelper
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import org.webrtc.VideoCapturer
|
||||
import org.webrtc.VideoSource
|
||||
import org.webrtc.VideoTrack
|
||||
import timber.log.Timber
|
||||
@ -78,6 +78,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
fun onCurrentCallChange(call: MxCall?)
|
||||
fun onCaptureStateChanged(captureInError: Boolean)
|
||||
fun onAudioDevicesChange(mgr: WebRtcPeerConnectionManager) {}
|
||||
fun onCameraChange(mgr: WebRtcPeerConnectionManager) {}
|
||||
}
|
||||
|
||||
private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList()
|
||||
@ -159,9 +160,10 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
|
||||
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
|
||||
// private var localSdp: SessionDescription? = null
|
||||
private var videoCapturer: CameraVideoCapturer? = null
|
||||
|
||||
private var videoCapturer: VideoCapturer? = null
|
||||
private val availableCamera = ArrayList<CameraProxy>()
|
||||
private var cameraInUse: CameraProxy? = null
|
||||
|
||||
var capturerIsInError = false
|
||||
set(value) {
|
||||
@ -413,51 +415,66 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
|
||||
// add video track if needed
|
||||
if (callContext.mxCall.isVideoCall) {
|
||||
availableCamera.clear()
|
||||
|
||||
val cameraIterator = if (Camera2Enumerator.isSupported(context)) Camera2Enumerator(context) else Camera1Enumerator(false)
|
||||
|
||||
// I don't realy know how that works if there are 2 front or 2 back cameras
|
||||
val frontCamera = cameraIterator.deviceNames
|
||||
?.firstOrNull { cameraIterator.isFrontFacing(it) }
|
||||
?: cameraIterator.deviceNames?.first()
|
||||
|
||||
// TODO detect when no camera or no front camera
|
||||
|
||||
val videoCapturer = cameraIterator.createCapturer(frontCamera, object : CameraEventsHandlerAdapter() {
|
||||
override fun onFirstFrameAvailable() {
|
||||
super.onFirstFrameAvailable()
|
||||
capturerIsInError = false
|
||||
}
|
||||
|
||||
override fun onCameraClosed() {
|
||||
// This could happen if you open the camera app in chat
|
||||
// We then register in order to restart capture as soon as the camera is available again
|
||||
Timber.v("## VOIP onCameraClosed")
|
||||
this@WebRtcPeerConnectionManager.capturerIsInError = true
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val restarter = CameraRestarter(frontCamera ?: "", callContext.mxCall.callId)
|
||||
callContext.cameraAvailabilityCallback = restarter
|
||||
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
cameraManager.registerAvailabilityCallback(restarter, null)
|
||||
?.let {
|
||||
CameraProxy(it, CameraType.FRONT).also { availableCamera.add(it) }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val videoSource = peerConnectionFactory!!.createVideoSource(videoCapturer.isScreencast)
|
||||
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
|
||||
Timber.v("## VOIP Local video source created")
|
||||
val backCamera = cameraIterator.deviceNames
|
||||
?.firstOrNull { cameraIterator.isBackFacing(it) }
|
||||
?.let {
|
||||
CameraProxy(it, CameraType.BACK).also { availableCamera.add(it) }
|
||||
}
|
||||
|
||||
videoCapturer.initialize(surfaceTextureHelper, context.applicationContext, videoSource!!.capturerObserver)
|
||||
// HD
|
||||
videoCapturer.startCapture(1280, 720, 30)
|
||||
val camera = frontCamera?.also { cameraInUse = frontCamera }
|
||||
?: backCamera?.also { cameraInUse = backCamera }
|
||||
?: null.also { cameraInUse = null }
|
||||
|
||||
this.videoCapturer = videoCapturer
|
||||
if (camera != null) {
|
||||
val videoCapturer = cameraIterator.createCapturer(camera.name, object : CameraEventsHandlerAdapter() {
|
||||
override fun onFirstFrameAvailable() {
|
||||
super.onFirstFrameAvailable()
|
||||
capturerIsInError = false
|
||||
}
|
||||
|
||||
val localVideoTrack = peerConnectionFactory!!.createVideoTrack("ARDAMSv0", videoSource)
|
||||
Timber.v("## VOIP Local video track created")
|
||||
localVideoTrack?.setEnabled(true)
|
||||
override fun onCameraClosed() {
|
||||
// This could happen if you open the camera app in chat
|
||||
// We then register in order to restart capture as soon as the camera is available again
|
||||
Timber.v("## VOIP onCameraClosed")
|
||||
this@WebRtcPeerConnectionManager.capturerIsInError = true
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val restarter = CameraRestarter(cameraInUse?.name ?: "", callContext.mxCall.callId)
|
||||
callContext.cameraAvailabilityCallback = restarter
|
||||
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
cameraManager.registerAvailabilityCallback(restarter, null)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
callContext.localVideoSource = videoSource
|
||||
callContext.localVideoTrack = localVideoTrack
|
||||
val videoSource = peerConnectionFactory!!.createVideoSource(videoCapturer.isScreencast)
|
||||
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
|
||||
Timber.v("## VOIP Local video source created")
|
||||
|
||||
localMediaStream?.addTrack(localVideoTrack)
|
||||
videoCapturer.initialize(surfaceTextureHelper, context.applicationContext, videoSource!!.capturerObserver)
|
||||
// HD
|
||||
videoCapturer.startCapture(1280, 720, 30)
|
||||
this.videoCapturer = videoCapturer
|
||||
|
||||
val localVideoTrack = peerConnectionFactory!!.createVideoTrack("ARDAMSv0", videoSource)
|
||||
Timber.v("## VOIP Local video track created")
|
||||
localVideoTrack?.setEnabled(true)
|
||||
|
||||
callContext.localVideoSource = videoSource
|
||||
callContext.localVideoTrack = localVideoTrack
|
||||
|
||||
localMediaStream?.addTrack(localVideoTrack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -661,6 +678,34 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
currentCall?.localVideoTrack?.setEnabled(enabled)
|
||||
}
|
||||
|
||||
fun switchCamera() {
|
||||
Timber.v("## VOIP switchCamera")
|
||||
if (currentCall != null && currentCall?.mxCall?.state is CallState.Connected && currentCall?.mxCall?.isVideoCall == true) {
|
||||
videoCapturer?.switchCamera(object : CameraVideoCapturer.CameraSwitchHandler {
|
||||
// Invoked on success. |isFrontCamera| is true if the new camera is front facing.
|
||||
override fun onCameraSwitchDone(isFrontCamera: Boolean) {
|
||||
Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera")
|
||||
cameraInUse = availableCamera.first { if (isFrontCamera) it.type == CameraType.FRONT else it.type == CameraType.BACK }
|
||||
currentCallsListeners.forEach {
|
||||
tryThis { it.onCameraChange(this@WebRtcPeerConnectionManager) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCameraSwitchError(errorDescription: String?) {
|
||||
Timber.v("## VOIP onCameraSwitchError isFront $errorDescription")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun canSwitchCamera(): Boolean {
|
||||
return availableCamera.size > 0
|
||||
}
|
||||
|
||||
fun currentCameraType(): CameraType? {
|
||||
return cameraInUse?.type
|
||||
}
|
||||
|
||||
fun endCall() {
|
||||
// Update service state
|
||||
CallService.onNoActiveCall(context)
|
||||
|
42
vector/src/main/res/drawable/ic_video_flip.xml
Normal file
42
vector/src/main/res/drawable/ic_video_flip.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4,9L1,12L4,15"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M10,15L13,12L10,9"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M13,12H2"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M23,7L16,12L23,17V7Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M1,7.5V7C1,5.8954 1.8954,5 3,5H14C15.1046,5 16,5.8954 16,7V17C16,18.1046 15.1046,19 14,19H3C1.8954,19 1,18.1046 1,17V16.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
@ -11,9 +11,18 @@
|
||||
android:id="@+id/callControlsSoundDevice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:actionTitle="Select sound device"
|
||||
app:actionTitle="@string/call_select_sound_device"
|
||||
tools:actionDescription="Speaker"
|
||||
app:leftIcon="@drawable/ic_call_speaker_default"
|
||||
app:tint="?attr/riotx_text_primary" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsSwitchCamera"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/call_switch_camera"
|
||||
tools:actionDescription="Front"
|
||||
app:leftIcon="@drawable/ic_video_flip"
|
||||
app:tint="?attr/riotx_text_primary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -219,6 +219,9 @@
|
||||
<string name="sound_device_speaker">Speaker</string>
|
||||
<string name="sound_device_headset">Headset</string>
|
||||
<string name="sound_device_wireless_headset">Wireless Headset</string>
|
||||
<string name="call_switch_camera">Switch Camera</string>
|
||||
<string name="call_camera_front">Front</string>
|
||||
<string name="call_camera_back">Back</string>
|
||||
|
||||
|
||||
<string name="option_send_files">Send files</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user