Adapting call activity layout for half opened mode on foldable devices

This commit is contained in:
Sylvain Berfini 2021-09-10 16:29:42 +02:00
parent a648ac3b1e
commit 1ccb60eec9
9 changed files with 247 additions and 82 deletions

View file

@ -49,8 +49,7 @@ abstract class GenericActivity : AppCompatActivity() {
val isDestructionPending: Boolean
get() = _isDestructionPending
open fun onLayoutChanges(foldingFeature: FoldingFeature?) {
}
open fun onLayoutChanges(foldingFeature: FoldingFeature?) { }
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {

View file

@ -24,16 +24,16 @@ import android.content.res.Configuration
import android.content.res.Resources
import android.os.Bundle
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import androidx.constraintlayout.widget.ConstraintSet
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.window.layout.FoldingFeature
import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.call.viewmodels.ControlsFadingViewModel
import org.linphone.activities.call.viewmodels.SharedCallViewModel
import org.linphone.activities.call.viewmodels.*
import org.linphone.activities.main.MainActivity
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
@ -44,9 +44,7 @@ class CallActivity : ProximitySensorActivity() {
private lateinit var viewModel: ControlsFadingViewModel
private lateinit var sharedViewModel: SharedCallViewModel
private var previewX: Float = 0f
private var previewY: Float = 0f
private lateinit var videoZoomHelper: VideoZoomHelper
private var foldingFeature: FoldingFeature? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -58,7 +56,7 @@ class CallActivity : ProximitySensorActivity() {
binding.lifecycleOwner = this
viewModel = ViewModelProvider(this).get(ControlsFadingViewModel::class.java)
binding.viewModel = viewModel
binding.controlsFadingViewModel = viewModel
sharedViewModel = ViewModelProvider(this).get(SharedCallViewModel::class.java)
@ -84,38 +82,24 @@ class CallActivity : ProximitySensorActivity() {
}
)
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
binding.setPreviewTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
previewX = v.x - event.rawX
previewY = v.y - event.rawY
}
MotionEvent.ACTION_MOVE -> {
v.animate()
.x(event.rawX + previewX)
.y(event.rawY + previewY)
.setDuration(0)
.start()
}
else -> {
v.performClick()
false
}
}
true
}
videoZoomHelper = VideoZoomHelper(this, binding.remoteVideoSurface)
viewModel.proximitySensorEnabled.observe(
this,
{
enableProximitySensor(it)
}
)
viewModel.videoEnabled.observe(
this,
{
updateConstraintSetDependingOnFoldingState()
}
)
}
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
this.foldingFeature = foldingFeature
updateConstraintSetDependingOnFoldingState()
}
override fun onResume() {
@ -206,4 +190,23 @@ class CallActivity : ProximitySensorActivity() {
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
}
private fun updateConstraintSetDependingOnFoldingState() {
val feature = foldingFeature ?: return
val constraintLayout = binding.constraintLayout
val set = ConstraintSet()
set.clone(constraintLayout)
if (feature.state == FoldingFeature.State.HALF_OPENED && viewModel.videoEnabled.value == true) {
set.setGuidelinePercent(R.id.hinge_top, 0.5f)
set.setGuidelinePercent(R.id.hinge_bottom, 0.5f)
viewModel.disable(true)
} else {
set.setGuidelinePercent(R.id.hinge_top, 0f)
set.setGuidelinePercent(R.id.hinge_bottom, 1f)
viewModel.disable(false)
}
set.applyTo(constraintLayout)
}
}

View file

@ -72,13 +72,19 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
ViewModelProvider(this).get(SharedCallViewModel::class.java)
}
callsViewModel = ViewModelProvider(this).get(CallsViewModel::class.java)
callsViewModel = requireActivity().run {
ViewModelProvider(this).get(CallsViewModel::class.java)
}
binding.viewModel = callsViewModel
controlsViewModel = ViewModelProvider(this).get(ControlsViewModel::class.java)
controlsViewModel = requireActivity().run {
ViewModelProvider(this).get(ControlsViewModel::class.java)
}
binding.controlsViewModel = controlsViewModel
conferenceViewModel = ViewModelProvider(this).get(ConferenceViewModel::class.java)
conferenceViewModel = requireActivity().run {
ViewModelProvider(this).get(ConferenceViewModel::class.java)
}
binding.conferenceViewModel = conferenceViewModel
callsViewModel.currentCallViewModel.observe(

View file

@ -0,0 +1,91 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.call.fragments
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import androidx.lifecycle.ViewModelProvider
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.call.VideoZoomHelper
import org.linphone.activities.call.viewmodels.CallsViewModel
import org.linphone.activities.call.viewmodels.ConferenceViewModel
import org.linphone.activities.call.viewmodels.ControlsFadingViewModel
import org.linphone.databinding.CallVideoFragmentBinding
class VideoRenderingFragment : GenericFragment<CallVideoFragmentBinding>() {
private lateinit var controlsFadingViewModel: ControlsFadingViewModel
private lateinit var callsViewModel: CallsViewModel
private lateinit var conferenceViewModel: ConferenceViewModel
private var previewX: Float = 0f
private var previewY: Float = 0f
private lateinit var videoZoomHelper: VideoZoomHelper
override fun getLayoutId(): Int = R.layout.call_video_fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = this
controlsFadingViewModel = requireActivity().run {
ViewModelProvider(this).get(ControlsFadingViewModel::class.java)
}
binding.controlsFadingViewModel = controlsFadingViewModel
callsViewModel = requireActivity().run {
ViewModelProvider(this).get(CallsViewModel::class.java)
}
conferenceViewModel = requireActivity().run {
ViewModelProvider(this).get(ConferenceViewModel::class.java)
}
binding.conferenceViewModel = conferenceViewModel
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
binding.setPreviewTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
previewX = v.x - event.rawX
previewY = v.y - event.rawY
}
MotionEvent.ACTION_MOVE -> {
v.animate()
.x(event.rawX + previewX)
.y(event.rawY + previewY)
.setDuration(0)
.start()
}
else -> {
v.performClick()
false
}
}
true
}
videoZoomHelper = VideoZoomHelper(requireContext(), binding.remoteVideoSurface)
}
}

View file

@ -58,7 +58,8 @@ class CallsViewModel : ViewModel() {
if (currentCall == null) {
currentCallViewModel.value?.destroy()
} else if (currentCallViewModel.value?.call != currentCall) {
currentCallViewModel.value = CallViewModel(currentCall)
val viewModel = CallViewModel(currentCall)
currentCallViewModel.value = viewModel
}
if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
@ -98,7 +99,9 @@ class CallsViewModel : ViewModel() {
noActiveCall.value = currentCall == null
if (currentCall != null) {
currentCallViewModel.value?.destroy()
currentCallViewModel.value = CallViewModel(currentCall)
val viewModel = CallViewModel(currentCall)
currentCallViewModel.value = viewModel
}
callPausedByRemote.value = currentCall?.state == Call.State.PausedByRemote

View file

@ -40,12 +40,16 @@ class ControlsFadingViewModel : ViewModel() {
val isVideoPreviewHidden = MutableLiveData<Boolean>()
val isVideoPreviewResizedForPip = MutableLiveData<Boolean>()
private val videoEnabled = MutableLiveData<Boolean>()
private val nonEarpieceOutputAudioDevice = MutableLiveData<Boolean>()
val videoEnabled = MutableLiveData<Boolean>()
val proximitySensorEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
private val nonEarpieceOutputAudioDevice = MutableLiveData<Boolean>()
private var timer: Timer? = null
private var disabled: Boolean = false
private val listener = object : CoreListenerStub() {
override fun onCallStateChanged(
core: Core,
@ -109,6 +113,15 @@ class ControlsFadingViewModel : ViewModel() {
startTimer()
}
fun disable(disable: Boolean) {
disabled = disable
if (disabled) {
stopTimer()
} else {
startTimer()
}
}
private fun shouldEnableProximitySensor(): Boolean {
return !(videoEnabled.value ?: false) && !(nonEarpieceOutputAudioDevice.value ?: false)
}
@ -121,6 +134,7 @@ class ControlsFadingViewModel : ViewModel() {
private fun startTimer() {
timer?.cancel()
if (disabled) return
timer = Timer("Hide UI controls scheduler")
timer?.schedule(

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/black_color"/>
<corners android:radius="6.7dp"/>
</shape>

View file

@ -1,14 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="previewTouchListener"
type="android.view.View.OnTouchListener" />
<variable
name="viewModel"
name="controlsFadingViewModel"
type="org.linphone.activities.call.viewmodels.ControlsFadingViewModel" />
</data>
@ -19,50 +17,54 @@
android:keepScreenOn="true"
android:background="?attr/backgroundColor">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextureView
android:onClick="@{() -> viewModel.showMomentarily()}"
android:id="@+id/remote_video_surface"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<org.linphone.mediastream.video.capture.CaptureTextureView
android:onTouch="@{previewTouchListener}"
android:visibility="@{viewModel.isVideoPreviewHidden ? View.INVISIBLE : View.VISIBLE, default=visible}"
android:id="@+id/local_preview_video_surface"
android:layout_size="@{viewModel.isVideoPreviewResizedForPip ? @dimen/video_preview_pip_max_size : @dimen/video_preview_max_size}"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true" />
<RelativeLayout
android:visibility="@{viewModel.areControlsHidden ? View.GONE : View.VISIBLE, default=visible}"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/video_rendering_fragment"
android:name="org.linphone.activities.call.fragments.VideoRenderingFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
tools:layout="@layout/call_video_fragment"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/hinge_bottom"/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/hinge_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/hinge_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="1" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/status_fragment"
android:name="org.linphone.activities.call.fragments.StatusFragment"
android:visibility="@{controlsFadingViewModel.areControlsHidden ? View.GONE : View.VISIBLE, default=visible}"
android:layout_width="match_parent"
android:layout_height="@dimen/status_fragment_size"
android:layout_alignParentTop="true"
tools:layout="@layout/call_status_fragment" />
tools:layout="@layout/call_status_fragment"
app:layout_constraintTop_toBottomOf="@id/hinge_top"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/call_controls_fragment"
android:name="org.linphone.activities.call.fragments.ControlsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/status_fragment"
android:layout_height="0dp"
android:visibility="@{controlsFadingViewModel.areControlsHidden ? View.GONE : View.VISIBLE, default=visible}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_fragment"
tools:layout="@layout/call_controls_fragment" />
</RelativeLayout>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Side Menu -->
<RelativeLayout

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="previewTouchListener"
type="android.view.View.OnTouchListener" />
<variable
name="controlsFadingViewModel"
type="org.linphone.activities.call.viewmodels.ControlsFadingViewModel" />
<variable
name="conferenceViewModel"
type="org.linphone.activities.call.viewmodels.ConferenceViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextureView
android:id="@+id/remote_video_surface"
android:onClick="@{() -> controlsFadingViewModel.showMomentarily()}"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<org.linphone.mediastream.video.capture.CaptureTextureView
android:onTouch="@{previewTouchListener}"
android:visibility="@{controlsFadingViewModel.isVideoPreviewHidden ? View.INVISIBLE : View.VISIBLE, default=visible}"
android:id="@+id/local_preview_video_surface"
android:layout_size="@{controlsFadingViewModel.isVideoPreviewResizedForPip ? @dimen/video_preview_pip_max_size : @dimen/video_preview_max_size}"
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>