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 val isDestructionPending: Boolean
get() = _isDestructionPending get() = _isDestructionPending
open fun onLayoutChanges(foldingFeature: FoldingFeature?) { open fun onLayoutChanges(foldingFeature: FoldingFeature?) { }
}
@SuppressLint("SourceLockedOrientationActivity") @SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View file

@ -24,16 +24,16 @@ import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.constraintlayout.widget.ConstraintSet
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.window.layout.FoldingFeature
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
import org.linphone.activities.call.viewmodels.ControlsFadingViewModel import org.linphone.activities.call.viewmodels.*
import org.linphone.activities.call.viewmodels.SharedCallViewModel
import org.linphone.activities.main.MainActivity import org.linphone.activities.main.MainActivity
import org.linphone.compatibility.Compatibility import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
@ -44,9 +44,7 @@ class CallActivity : ProximitySensorActivity() {
private lateinit var viewModel: ControlsFadingViewModel private lateinit var viewModel: ControlsFadingViewModel
private lateinit var sharedViewModel: SharedCallViewModel private lateinit var sharedViewModel: SharedCallViewModel
private var previewX: Float = 0f private var foldingFeature: FoldingFeature? = null
private var previewY: Float = 0f
private lateinit var videoZoomHelper: VideoZoomHelper
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -58,7 +56,7 @@ class CallActivity : ProximitySensorActivity() {
binding.lifecycleOwner = this binding.lifecycleOwner = this
viewModel = ViewModelProvider(this).get(ControlsFadingViewModel::class.java) viewModel = ViewModelProvider(this).get(ControlsFadingViewModel::class.java)
binding.viewModel = viewModel binding.controlsFadingViewModel = viewModel
sharedViewModel = ViewModelProvider(this).get(SharedCallViewModel::class.java) 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( viewModel.proximitySensorEnabled.observe(
this, this,
{ {
enableProximitySensor(it) enableProximitySensor(it)
} }
) )
viewModel.videoEnabled.observe(
this,
{
updateConstraintSetDependingOnFoldingState()
}
)
}
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
this.foldingFeature = foldingFeature
updateConstraintSetDependingOnFoldingState()
} }
override fun onResume() { override fun onResume() {
@ -206,4 +190,23 @@ class CallActivity : ProximitySensorActivity() {
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 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) 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 binding.viewModel = callsViewModel
controlsViewModel = ViewModelProvider(this).get(ControlsViewModel::class.java) controlsViewModel = requireActivity().run {
ViewModelProvider(this).get(ControlsViewModel::class.java)
}
binding.controlsViewModel = controlsViewModel binding.controlsViewModel = controlsViewModel
conferenceViewModel = ViewModelProvider(this).get(ConferenceViewModel::class.java) conferenceViewModel = requireActivity().run {
ViewModelProvider(this).get(ConferenceViewModel::class.java)
}
binding.conferenceViewModel = conferenceViewModel binding.conferenceViewModel = conferenceViewModel
callsViewModel.currentCallViewModel.observe( 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) { if (currentCall == null) {
currentCallViewModel.value?.destroy() currentCallViewModel.value?.destroy()
} else if (currentCallViewModel.value?.call != currentCall) { } 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) { if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
@ -98,7 +99,9 @@ class CallsViewModel : ViewModel() {
noActiveCall.value = currentCall == null noActiveCall.value = currentCall == null
if (currentCall != null) { if (currentCall != null) {
currentCallViewModel.value?.destroy() currentCallViewModel.value?.destroy()
currentCallViewModel.value = CallViewModel(currentCall)
val viewModel = CallViewModel(currentCall)
currentCallViewModel.value = viewModel
} }
callPausedByRemote.value = currentCall?.state == Call.State.PausedByRemote callPausedByRemote.value = currentCall?.state == Call.State.PausedByRemote

View file

@ -40,12 +40,16 @@ class ControlsFadingViewModel : ViewModel() {
val isVideoPreviewHidden = MutableLiveData<Boolean>() val isVideoPreviewHidden = MutableLiveData<Boolean>()
val isVideoPreviewResizedForPip = MutableLiveData<Boolean>() val isVideoPreviewResizedForPip = MutableLiveData<Boolean>()
private val videoEnabled = MutableLiveData<Boolean>() val videoEnabled = MutableLiveData<Boolean>()
private val nonEarpieceOutputAudioDevice = MutableLiveData<Boolean>()
val proximitySensorEnabled: MediatorLiveData<Boolean> = MediatorLiveData() val proximitySensorEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
private val nonEarpieceOutputAudioDevice = MutableLiveData<Boolean>()
private var timer: Timer? = null private var timer: Timer? = null
private var disabled: Boolean = false
private val listener = object : CoreListenerStub() { private val listener = object : CoreListenerStub() {
override fun onCallStateChanged( override fun onCallStateChanged(
core: Core, core: Core,
@ -109,6 +113,15 @@ class ControlsFadingViewModel : ViewModel() {
startTimer() startTimer()
} }
fun disable(disable: Boolean) {
disabled = disable
if (disabled) {
stopTimer()
} else {
startTimer()
}
}
private fun shouldEnableProximitySensor(): Boolean { private fun shouldEnableProximitySensor(): Boolean {
return !(videoEnabled.value ?: false) && !(nonEarpieceOutputAudioDevice.value ?: false) return !(videoEnabled.value ?: false) && !(nonEarpieceOutputAudioDevice.value ?: false)
} }
@ -121,6 +134,7 @@ class ControlsFadingViewModel : ViewModel() {
private fun startTimer() { private fun startTimer() {
timer?.cancel() timer?.cancel()
if (disabled) return
timer = Timer("Hide UI controls scheduler") timer = Timer("Hide UI controls scheduler")
timer?.schedule( 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"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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> <data>
<import type="android.view.View" /> <import type="android.view.View" />
<variable <variable
name="previewTouchListener" name="controlsFadingViewModel"
type="android.view.View.OnTouchListener" />
<variable
name="viewModel"
type="org.linphone.activities.call.viewmodels.ControlsFadingViewModel" /> type="org.linphone.activities.call.viewmodels.ControlsFadingViewModel" />
</data> </data>
@ -19,50 +17,54 @@
android:keepScreenOn="true" android:keepScreenOn="true"
android:background="?attr/backgroundColor"> android:background="?attr/backgroundColor">
<RelativeLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraint_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextureView <androidx.fragment.app.FragmentContainerView
android:onClick="@{() -> viewModel.showMomentarily()}" android:id="@+id/video_rendering_fragment"
android:id="@+id/remote_video_surface" android:name="org.linphone.activities.call.fragments.VideoRenderingFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="0dp"
tools:layout="@layout/call_video_fragment"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/hinge_bottom"/>
<org.linphone.mediastream.video.capture.CaptureTextureView <androidx.constraintlayout.widget.Guideline
android:onTouch="@{previewTouchListener}" android:id="@+id/hinge_top"
android:visibility="@{viewModel.isVideoPreviewHidden ? View.INVISIBLE : View.VISIBLE, default=visible}" android:layout_width="wrap_content"
android:id="@+id/local_preview_video_surface" android:layout_height="wrap_content"
android:layout_size="@{viewModel.isVideoPreviewResizedForPip ? @dimen/video_preview_pip_max_size : @dimen/video_preview_max_size}" android:orientation="horizontal"
android:layout_width="200dp" app:layout_constraintGuide_percent="0" />
android:layout_height="200dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true" />
<RelativeLayout <androidx.constraintlayout.widget.Guideline
android:visibility="@{viewModel.areControlsHidden ? View.GONE : View.VISIBLE, default=visible}" 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_width="match_parent"
android:layout_height="match_parent"> android:layout_height="@dimen/status_fragment_size"
tools:layout="@layout/call_status_fragment"
app:layout_constraintTop_toBottomOf="@id/hinge_top"/>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/status_fragment" android:id="@+id/call_controls_fragment"
android:name="org.linphone.activities.call.fragments.StatusFragment" android:name="org.linphone.activities.call.fragments.ControlsFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/status_fragment_size" android:layout_height="0dp"
android:layout_alignParentTop="true" android:visibility="@{controlsFadingViewModel.areControlsHidden ? View.GONE : View.VISIBLE, default=visible}"
tools:layout="@layout/call_status_fragment" /> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_fragment"
tools:layout="@layout/call_controls_fragment" />
<androidx.fragment.app.FragmentContainerView </androidx.constraintlayout.widget.ConstraintLayout>
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"
tools:layout="@layout/call_controls_fragment" />
</RelativeLayout>
</RelativeLayout>
<!-- Side Menu --> <!-- Side Menu -->
<RelativeLayout <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>