diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 85dae6730..e6ca53974 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -177,6 +177,30 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") { Log.i("[Context] Starting call $call") } + fun switchCamera() { + val currentDevice = core.videoDevice + Log.i("[Context] Current camera device is $currentDevice") + + for (camera in core.videoDevicesList) { + if (camera != currentDevice && camera != "StaticImage: Static picture") { + Log.i("[Context] New camera device will be $camera") + core.videoDevice = camera + break + } + } + + val call = core.currentCall + if (call == null) { + Log.w("[Context] Switching camera while not in call") + return + } + call.update(null) + } + + fun showSwitchCameraButton(): Boolean { + return core.videoDevicesList.size > 2 // Count StaticImage camera + } + private fun showCallActivity() { Log.i("[Context] Starting VoIP activity") val intent = Intent(context, VoipActivity::class.java) diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index 31c4e3cbc..834fb4093 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -34,6 +34,8 @@ import org.linphone.databinding.MainActivityBinding class MainActivity : AppCompatActivity() { companion object { private const val CONTACTS_PERMISSION_REQUEST = 0 + private const val CAMERA_PERMISSION_REQUEST = 1 + private const val RECORD_AUDIO_PERMISSION_REQUEST = 2 } private lateinit var binding: MainActivityBinding @@ -54,6 +56,8 @@ class MainActivity : AppCompatActivity() { if (checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { loadContacts() } + checkSelfPermission(Manifest.permission.CAMERA) + checkSelfPermission(Manifest.permission.RECORD_AUDIO) binding = DataBindingUtil.setContentView(this, R.layout.main_activity) binding.lifecycleOwner = this @@ -68,6 +72,18 @@ class MainActivity : AppCompatActivity() { CONTACTS_PERMISSION_REQUEST ) } + if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + arrayOf(Manifest.permission.CAMERA), + CAMERA_PERMISSION_REQUEST + ) + } + if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + arrayOf(Manifest.permission.RECORD_AUDIO), + RECORD_AUDIO_PERMISSION_REQUEST + ) + } } override fun onRequestPermissionsResult( diff --git a/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt index 30f6ebac0..46b765b98 100644 --- a/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/voip/fragment/ActiveCallFragment.kt @@ -22,9 +22,11 @@ package org.linphone.ui.voip.fragment import android.os.Bundle import android.os.SystemClock import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.databinding.VoipActiveCallFragmentBinding import org.linphone.ui.main.fragment.GenericFragment @@ -38,6 +40,33 @@ class ActiveCallFragment : GenericFragment() { private lateinit var callViewModel: CurrentCallViewModel + // For moving video preview purposes + + private var previewX: Float = 0f + private var previewY: Float = 0f + + private val previewTouchListener = View.OnTouchListener { view, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + previewX = view.x - event.rawX + previewY = view.y - event.rawY + true + } + MotionEvent.ACTION_MOVE -> { + view.animate() + .x(event.rawX + previewX) + .y(event.rawY + previewY) + .setDuration(0) + .start() + true + } + else -> { + view.performClick() + false + } + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -101,4 +130,19 @@ class ActiveCallFragment : GenericFragment() { binding.chronometer.start() } } + + override fun onResume() { + super.onResume() + + coreContext.postOnCoreThread { core -> + core.nativeVideoWindowId = binding.remoteVideoSurface + coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface + binding.localPreviewVideoSurface.setOnTouchListener(previewTouchListener) + } + } + + override fun onPause() { + super.onPause() + binding.localPreviewVideoSurface.setOnTouchListener(null) + } } diff --git a/app/src/main/java/org/linphone/ui/voip/view/RoundCornersTextureView.kt b/app/src/main/java/org/linphone/ui/voip/view/RoundCornersTextureView.kt new file mode 100644 index 000000000..728656604 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/voip/view/RoundCornersTextureView.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * 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 . + */ +package org.linphone.ui.voip.view + +import android.content.Context +import android.graphics.Outline +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import android.view.ViewOutlineProvider +import org.linphone.R +import org.linphone.mediastream.video.capture.CaptureTextureView + +class RoundCornersTextureView : CaptureTextureView { + private var mRadius: Float = 0f + + constructor(context: Context) : super(context) { + mAlignTopRight = true + mDisplayMode = DisplayMode.BLACK_BARS + setRoundCorners() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + readAttributes(attrs) + setRoundCorners() + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + readAttributes(attrs) + setRoundCorners() + } + + private fun readAttributes(attrs: AttributeSet) { + context.theme.obtainStyledAttributes( + attrs, + R.styleable.RoundCornersTextureView, + 0, + 0 + ).apply { + try { + mAlignTopRight = getBoolean(R.styleable.RoundCornersTextureView_alignTopRight, true) + val mode = getInteger( + R.styleable.RoundCornersTextureView_displayMode, + DisplayMode.BLACK_BARS.ordinal + ) + mDisplayMode = when (mode) { + 1 -> DisplayMode.OCCUPY_ALL_SPACE + 2 -> DisplayMode.HYBRID + else -> DisplayMode.BLACK_BARS + } + mRadius = getFloat( + R.styleable.RoundCornersTextureView_radius, + context.resources.getDimension( + R.dimen.in_call_round_corners_texture_view_radius + ) + ) + } finally { + recycle() + } + } + } + + private fun setRoundCorners() { + outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + val rect = if (previewRectF != null && + actualDisplayMode == DisplayMode.BLACK_BARS && + mAlignTopRight + ) { + Rect( + previewRectF.left.toInt(), + previewRectF.top.toInt(), + previewRectF.right.toInt(), + previewRectF.bottom.toInt() + ) + } else { + Rect( + 0, + 0, + width, + height + ) + } + outline.setRoundRect(rect, mRadius) + } + } + clipToOutline = true + } + + override fun setAspectRatio(width: Int, height: Int) { + super.setAspectRatio(width, height) + + val previewSize = previewVideoSize + if (previewSize.width > 0 && previewSize.height > 0) { + setRoundCorners() + } + } +} diff --git a/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt index 83110755d..4e637efbe 100644 --- a/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/voip/viewmodel/CurrentCallViewModel.kt @@ -27,6 +27,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.core.Call import org.linphone.core.CallListenerStub +import org.linphone.core.MediaDirection import org.linphone.core.MediaEncryption import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel @@ -50,14 +51,19 @@ class CurrentCallViewModel() : ViewModel() { val isMicrophoneMuted = MutableLiveData() + val fullScreenMode = MutableLiveData() + + // To synchronize chronometers in UI + val callDuration = MutableLiveData() + + // ZRTP related + val isRemoteDeviceTrusted = MutableLiveData() val showZrtpSasDialogEvent: MutableLiveData>> by lazy { MutableLiveData>>() } - val callDuration = MutableLiveData() - // Extras actions val isActionsMenuExpanded = MutableLiveData() @@ -88,11 +94,20 @@ class CurrentCallViewModel() : ViewModel() { override fun onEncryptionChanged(call: Call, on: Boolean, authenticationToken: String?) { updateEncryption() } + + override fun onStateChanged(call: Call, state: Call.State?, message: String) { + if (LinphoneUtils.isCallOutgoing(call.state)) { + isVideoEnabled.postValue(call.params.isVideoEnabled) + } else { + isVideoEnabled.postValue(call.currentParams.isVideoEnabled) + } + } } init { isVideoEnabled.value = false isMicrophoneMuted.value = false + fullScreenMode.value = false isActionsMenuExpanded.value = false extraActionsMenuTranslateY.value = extraActionsMenuHeight @@ -119,6 +134,14 @@ class CurrentCallViewModel() : ViewModel() { } } + fun answer() { + // UI thread + coreContext.postOnCoreThread { + Log.i("$TAG Answering call [$call]") + call.accept() + } + } + fun hangUp() { // UI thread coreContext.postOnCoreThread { @@ -154,7 +177,38 @@ class CurrentCallViewModel() : ViewModel() { // UI thread // TODO: check video permission - // TODO + coreContext.postOnCoreThread { core -> + if (::call.isInitialized) { + val params = core.createCallParams(call) + if (call.conference != null) { + if (params?.isVideoEnabled == false) { + params.isVideoEnabled = true + params.videoDirection = MediaDirection.SendRecv + } else { + if (params?.videoDirection == MediaDirection.SendRecv || params?.videoDirection == MediaDirection.SendOnly) { + params.videoDirection = MediaDirection.RecvOnly + } else { + params?.videoDirection = MediaDirection.SendRecv + } + } + } else { + params?.isVideoEnabled = params?.isVideoEnabled == false + Log.i( + "$TAG Updating call with video enabled set to ${params?.isVideoEnabled}" + ) + } + call.update(params) + } + } + } + + fun switchCamera() { + coreContext.switchCamera() + } + + fun toggleFullScreen() { + if (fullScreenMode.value == false && isVideoEnabled.value == false) return + fullScreenMode.value = fullScreenMode.value != true } fun toggleExpandActionsMenu() { diff --git a/app/src/main/res/drawable/shape_answer_button_background.xml b/app/src/main/res/drawable/shape_answer_button_background.xml new file mode 100644 index 000000000..3b8ff1a6a --- /dev/null +++ b/app/src/main/res/drawable/shape_answer_button_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_active_call_fragment.xml b/app/src/main/res/layout/voip_active_call_fragment.xml index c34a834e6..9bc263c66 100644 --- a/app/src/main/res/layout/voip_active_call_fragment.xml +++ b/app/src/main/res/layout/voip_active_call_fragment.xml @@ -65,13 +65,14 @@ app:layout_constraintBottom_toBottomOf="@id/call_direction_label"/> + + + + @@ -74,6 +75,7 @@ app:layout_constraintEnd_toStartOf="@id/change_audio_output" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_outgoing_call_fragment.xml b/app/src/main/res/layout/voip_outgoing_call_fragment.xml index 960bb7658..ac999da73 100644 --- a/app/src/main/res/layout/voip_outgoing_call_fragment.xml +++ b/app/src/main/res/layout/voip_outgoing_call_fragment.xml @@ -40,13 +40,14 @@ app:layout_constraintTop_toTopOf="parent"/> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 5d74d6bcd..a76a5d803 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -14,6 +14,7 @@ 116dp 237dp 353dp + 20dp 360dp \ No newline at end of file