Reworked in-call fragment to properly handle foldable phones

This commit is contained in:
Sylvain Berfini 2023-09-11 14:02:28 +02:00
parent 0dbc9f7a8a
commit 18254fd385
6 changed files with 163 additions and 85 deletions

View file

@ -76,6 +76,7 @@ dependencies {
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
implementation "androidx.core:core-telecom:1.0.0-alpha01"
implementation 'androidx.window:window:1.1.0'
def nav_version = "2.7.2"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"

View file

@ -33,6 +33,11 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication
import org.linphone.R
import org.linphone.core.tools.Log
@ -78,6 +83,15 @@ class VoipActivity : AppCompatActivity() {
binding = DataBindingUtil.setContentView(this, R.layout.voip_activity)
binding.lifecycleOwner = this
lifecycleScope.launch(Dispatchers.Main) {
WindowInfoTracker
.getOrCreate(this@VoipActivity)
.windowLayoutInfo(this@VoipActivity)
.collect { newLayoutInfo ->
updateCurrentLayout(newLayoutInfo)
}
}
sharedViewModel = run {
ViewModelProvider(this)[SharedCallViewModel::class.java]
}
@ -158,6 +172,20 @@ class VoipActivity : AppCompatActivity() {
}
}
private fun updateCurrentLayout(newLayoutInfo: WindowLayoutInfo) {
if (newLayoutInfo.displayFeatures.isNotEmpty()) {
for (feature in newLayoutInfo.displayFeatures) {
val foldingFeature = feature as? FoldingFeature
if (foldingFeature != null) {
Log.i(
"$TAG Folding feature state changed: ${foldingFeature.state}, orientation is ${foldingFeature.orientation}"
)
sharedViewModel.foldingState.value = foldingFeature
}
}
}
}
fun showBlueToast(message: String, @DrawableRes icon: Int) {
val blueToast = AppUtils.getBlueToast(this, binding.toastsArea, message, icon)
binding.toastsArea.addView(blueToast.root)

View file

@ -26,7 +26,9 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.ViewModelProvider
import androidx.window.layout.FoldingFeature
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
@ -96,13 +98,17 @@ class ActiveCallFragment : GenericCallFragment() {
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = callViewModel
val standardBottomSheetBehavior = BottomSheetBehavior.from(binding.bottomBar.root)
standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
val bottomSheetBehavior = BottomSheetBehavior.from(binding.bottomBar.root)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
sharedViewModel = requireActivity().run {
ViewModelProvider(this)[SharedCallViewModel::class.java]
}
sharedViewModel.foldingState.observe(viewLifecycleOwner) { feature ->
updateHingeRelatedConstraints(feature)
}
callViewModel.fullScreenMode.observe(viewLifecycleOwner) { hide ->
Log.i("$TAG Switching full screen mode to ${if (hide) "ON" else "OFF"}")
sharedViewModel.toggleFullScreenEvent.value = Event(hide)
@ -154,16 +160,16 @@ class ActiveCallFragment : GenericCallFragment() {
callViewModel.toggleExtraActionsBottomSheetEvent.observe(viewLifecycleOwner) {
it.consume {
val state = standardBottomSheetBehavior.state
val state = bottomSheetBehavior.state
if (state == BottomSheetBehavior.STATE_COLLAPSED) {
standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} else if (state == BottomSheetBehavior.STATE_EXPANDED) {
standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
}
standardBottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
when (newState) {
BottomSheetBehavior.STATE_COLLAPSED, BottomSheetBehavior.STATE_HIDDEN -> {
@ -194,4 +200,24 @@ class ActiveCallFragment : GenericCallFragment() {
super.onPause()
binding.localPreviewVideoSurface.setOnTouchListener(null)
}
private fun updateHingeRelatedConstraints(feature: FoldingFeature) {
Log.i("$TAG Updating constraint layout hinges: $feature")
val constraintLayout = binding.constraintLayout
val set = ConstraintSet()
set.clone(constraintLayout)
if (feature.state == FoldingFeature.State.HALF_OPENED) {
set.setGuidelinePercent(R.id.hinge_top, 0.5f)
set.setGuidelinePercent(R.id.hinge_bottom, 0.5f)
sharedViewModel.folded.value = true
} else {
set.setGuidelinePercent(R.id.hinge_top, 0f)
set.setGuidelinePercent(R.id.hinge_bottom, 1f)
sharedViewModel.folded.value = false
}
set.applyTo(constraintLayout)
}
}

View file

@ -22,8 +22,13 @@ package org.linphone.ui.voip.viewmodel
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.window.layout.FoldingFeature
import org.linphone.utils.Event
class SharedCallViewModel @UiThread constructor() : ViewModel() {
val toggleFullScreenEvent = MutableLiveData<Event<Boolean>>()
val foldingState = MutableLiveData<FoldingFeature>()
val folded = MutableLiveData<Boolean>()
}

View file

@ -15,10 +15,104 @@
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/in_call_black">
<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" />
<ImageView
android:id="@+id/background"
android:src="@drawable/shape_round_in_call_gray_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="@dimen/in_call_main_actions_menu_height"
app:layout_constraintTop_toBottomOf="@id/call_direction_label"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_in_call_size"
android:layout_height="@dimen/avatar_in_call_size"
android:background="@drawable/shape_avatar_background"
contactAvatar="@{viewModel.contact}"
app:avatarViewInitialsBackgroundColor="@color/blue_outgoing_message"
app:avatarViewInitialsTextColor="@color/gray_9"
app:avatarViewInitialsTextSize="36sp"
app:avatarViewInitialsTextStyle="bold"
app:avatarViewPlaceholder="@drawable/contact_avatar"
app:avatarViewShape="circle"
app:avatarViewBorderWidth="0dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintEnd_toEndOf="@id/background"
app:layout_constraintStart_toStartOf="@id/background"
app:layout_constraintTop_toBottomOf="@id/call_direction_label"
app:layout_constraintBottom_toTopOf="@id/name"/>
<ImageView
android:id="@+id/trust_badge"
android:layout_width="@dimen/avatar_presence_badge_in_call_size"
android:layout_height="@dimen/avatar_presence_badge_in_call_size"
android:src="@drawable/trusted"
android:visibility="@{viewModel.contact.showTrust ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@{viewModel.displayedName, default=`John Doe`}"
android:textColor="@color/white"
android:textSize="22sp"
app:layout_constraintTop_toBottomOf="@id/avatar"
app:layout_constraintBottom_toTopOf="@id/address"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintBottom_toBottomOf="@id/background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<org.linphone.ui.voip.view.RoundCornersTextureView
android:id="@+id/remote_video_surface"
android:layout_width="0dp"
android:layout_height="0dp"
android:onClick="@{() -> viewModel.toggleFullScreen()}"
android:visibility="@{viewModel.isVideoEnabled ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/hinge_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<ImageView
android:id="@+id/call_direction_icon"
android:layout_width="@dimen/icon_size"
@ -44,7 +138,7 @@
android:textColor="@color/white"
android:textSize="16sp"
app:layout_constraintStart_toEndOf="@id/call_direction_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toTopOf="@id/hinge_top"
app:layout_constraintBottom_toTopOf="@id/background"/>
<androidx.appcompat.widget.AppCompatTextView
@ -83,88 +177,11 @@
android:layout_marginEnd="10dp"
android:src="@drawable/camera_rotate"
android:visibility="@{!viewModel.fullScreenMode &amp;&amp; viewModel.isVideoEnabled &amp;&amp; viewModel.showSwitchCamera ? View.VISIBLE : View.GONE}"
app:tint="@color/white"
app:layout_constraintTop_toTopOf="@id/call_direction_label"
app:layout_constraintBottom_toBottomOf="@id/call_direction_label"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white" />
<ImageView
android:id="@+id/background"
android:src="@drawable/shape_round_in_call_gray_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="@dimen/in_call_main_actions_menu_height"
app:layout_constraintTop_toBottomOf="@id/call_direction_label"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_in_call_size"
android:layout_height="@dimen/avatar_in_call_size"
android:background="@drawable/shape_avatar_background"
contactAvatar="@{viewModel.contact}"
app:avatarViewInitialsBackgroundColor="@color/blue_outgoing_message"
app:avatarViewInitialsTextColor="@color/gray_9"
app:avatarViewInitialsTextSize="36sp"
app:avatarViewInitialsTextStyle="bold"
app:avatarViewPlaceholder="@drawable/contact_avatar"
app:avatarViewShape="circle"
app:avatarViewBorderWidth="0dp"
app:layout_constraintEnd_toEndOf="@id/background"
app:layout_constraintStart_toStartOf="@id/background"
app:layout_constraintTop_toTopOf="@id/background"
app:layout_constraintBottom_toBottomOf="@id/background"/>
<ImageView
android:id="@+id/trust_badge"
android:layout_width="@dimen/avatar_presence_badge_in_call_size"
android:layout_height="@dimen/avatar_presence_badge_in_call_size"
android:src="@drawable/trusted"
android:visibility="@{viewModel.contact.showTrust ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@{viewModel.displayedName, default=`John Doe`}"
android:textColor="@color/white"
android:textSize="22sp"
app:layout_constraintTop_toBottomOf="@id/avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<org.linphone.ui.voip.view.RoundCornersTextureView
android:id="@+id/remote_video_surface"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="@{viewModel.fullScreenMode ? @dimen/zero : @dimen/in_call_main_actions_menu_height, default=@dimen/in_call_main_actions_menu_height}"
android:onClick="@{() -> viewModel.toggleFullScreen()}"
android:visibility="@{viewModel.isVideoEnabled ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/background" />
<org.linphone.ui.voip.view.RoundCornersTextureView
android:id="@+id/local_preview_video_surface"
android:layout_width="wrap_content"

View file

@ -9,6 +9,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true"
tools:context=".ui.voip.VoipActivity">
<androidx.constraintlayout.widget.ConstraintLayout