Webrtc call: get back on green banner

This commit is contained in:
ganfra 2021-07-12 17:49:44 +02:00
parent 74915c1e9e
commit d973cd7848
9 changed files with 204 additions and 303 deletions

View File

@ -1,156 +0,0 @@
/*
* Copyright (c) 2021 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.app.core.ui.views
import android.content.Context
import android.util.AttributeSet
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.google.android.material.card.MaterialCardView
import im.vector.app.R
import im.vector.app.databinding.ViewCurrentCallsCardBinding
import im.vector.app.features.call.utils.EglUtils
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem
import im.vector.app.features.home.AvatarRenderer
import io.github.hyuwah.draggableviewlib.DraggableView
import io.github.hyuwah.draggableviewlib.setupDraggable
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState
import org.webrtc.RendererCommon
class CurrentCallsCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : MaterialCardView(context, attrs, defStyleAttr) {
interface Callback {
fun onTapToReturnToCall()
}
private val views: ViewCurrentCallsCardBinding
private var activeCallPipInitialized = false
private var currentCall: WebRtcCall? = null
private var draggableView: DraggableView<CurrentCallsCardView>? = null
lateinit var avatarRenderer: AvatarRenderer
lateinit var session: Session
var callback: Callback? = null
init {
inflate(context, R.layout.view_current_calls_card, this)
isVisible = false
views = ViewCurrentCallsCardBinding.bind(this)
draggableView = setupDraggable().build()
setOnClickListener { callback?.onTapToReturnToCall() }
}
fun render(currentCall: WebRtcCall?, calls: List<WebRtcCall>) {
views.activeCallPiP.let {
this.currentCall?.detachRenderers(listOf(it))
}
this.currentCall = currentCall
if (currentCall != null) {
isVisible = true
when (currentCall.mxCall.state) {
is CallState.LocalRinging, CallState.Idle -> {
isVisible = false
}
is CallState.Connected -> {
views.activeCallProgress.isVisible = false
val isVideoCall = currentCall.mxCall.isVideoCall
if (isVideoCall) {
renderVideoCall(currentCall)
} else {
renderVoiceCall(currentCall, calls)
}
}
else -> {
renderConnectingState(currentCall)
}
}
} else {
// NO ACTIVE CALL
isVisible = false
}
}
private fun renderConnectingState(currentCall: WebRtcCall) {
//TODO show dots
views.activeCallProgress.isVisible = true
views.activeCallPiP.isVisible = false
views.avatarViews.isVisible = false
currentCall.detachRenderers(listOf(views.activeCallPiP))
}
private fun renderVideoCall(currentCall: WebRtcCall) {
initIfNeeded()
views.activeCallPiP.isVisible = true
views.avatarViews.isVisible = false
currentCall.attachViewRenderers(null, views.activeCallPiP, null)
}
private fun renderVoiceCall(currentCall: WebRtcCall, calls: List<WebRtcCall>) {
views.activeCallPiP.isVisible = false
views.avatarViews.isVisible = true
val isActiveCallPaused = currentCall.isLocalOnHold || currentCall.isRemoteOnHold
views.activeCallPausedIcon.isVisible = isActiveCallPaused
val activeOpponentMatrixItem = currentCall.getOpponentAsMatrixItem(session)
if (isActiveCallPaused) {
val colorFilter = ContextCompat.getColor(context, R.color.bg_call_screen_blur)
activeOpponentMatrixItem?.also {
avatarRenderer.renderBlur(it, views.activeCallOpponentAvatar, sampling = 2, rounded = true, colorFilter = colorFilter, addPlaceholder = true)
}
} else {
activeOpponentMatrixItem?.also {
avatarRenderer.render(it, views.activeCallOpponentAvatar)
}
}
val otherConnectedCall = calls.filter {
it.mxCall.state is CallState.Connected
}.firstOrNull {
it != currentCall
}
if (otherConnectedCall != null) {
views.otherCallOpponentAvatar.isVisible = true
views.otherCallPausedIcon.isVisible = true
otherConnectedCall.getOpponentAsMatrixItem(session)?.also { heldOpponentMatrixItem ->
avatarRenderer.render(heldOpponentMatrixItem, views.activeCallOpponentAvatar)
}
} else {
views.otherCallOpponentAvatar.isVisible = false
views.otherCallPausedIcon.isVisible = false
}
}
private fun initIfNeeded() {
if (!activeCallPipInitialized) {
EglUtils.rootEglBase?.let { eglBase ->
views.activeCallPiP.apply {
init(eglBase.eglBaseContext, null)
setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_BALANCED)
setEnableHardwareScaler(true)
setZOrderMediaOverlay(true)
}
activeCallPipInitialized = true
}
}
}
}

View File

@ -0,0 +1,75 @@
/*
* 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.app.core.ui.views
import android.content.Context
import android.content.res.ColorStateList
import android.os.Build
import android.util.AttributeSet
import android.util.TypedValue
import android.widget.FrameLayout
import android.widget.RelativeLayout
import androidx.appcompat.content.res.AppCompatResources
import im.vector.app.R
import im.vector.app.databinding.ViewCurrentCallsBinding
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.call.CallState
class CurrentCallsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onTapToReturnToCall()
}
val views: ViewCurrentCallsBinding
var callback: Callback? = null
init {
inflate(context, R.layout.view_current_calls, this)
views = ViewCurrentCallsBinding.bind(this)
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
val outValue = TypedValue().also {
context.theme.resolveAttribute(android.R.attr.selectableItemBackground, it, true)
}
foreground = AppCompatResources.getDrawable(context, outValue.resourceId)
setOnClickListener { callback?.onTapToReturnToCall() }
}
fun render(calls: List<WebRtcCall>, formattedDuration: String) {
val connectedCalls = calls.filter {
it.mxCall.state is CallState.Connected
}
val heldCalls = connectedCalls.filter {
it.isLocalOnHold || it.isRemoteOnHold
}
if (connectedCalls.isEmpty()) return
views.currentCallsInfo.text = if (connectedCalls.size == heldCalls.size) {
resources.getQuantityString(R.plurals.call_only_paused, heldCalls.size, heldCalls.size)
} else {
if (heldCalls.isEmpty()) {
resources.getString(R.string.call_only_active, formattedDuration)
} else {
resources.getQuantityString(R.plurals.call_one_active_and_other_paused, heldCalls.size, formattedDuration, heldCalls.size)
}
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2021 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.app.core.ui.views
import androidx.core.view.isVisible
import im.vector.app.features.call.webrtc.WebRtcCall
import org.matrix.android.sdk.api.session.call.CallState
class CurrentCallsViewPresenter {
private var currentCallsView: CurrentCallsView? = null
private var currentCall: WebRtcCall? = null
private var calls: List<WebRtcCall> = emptyList()
private val tickListener = object : WebRtcCall.Listener {
override fun onTick(formattedDuration: String) {
currentCallsView?.render(calls, formattedDuration)
}
}
fun updateCall(currentCall: WebRtcCall?, calls: List<WebRtcCall>) {
this.currentCall?.removeListener(tickListener)
this.currentCall = currentCall
this.currentCall?.addListener(tickListener)
this.calls = calls
val hasActiveCall = currentCall?.mxCall?.state is CallState.Connected
currentCallsView?.isVisible = hasActiveCall
if (hasActiveCall) {
currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "")
} else {
currentCallsView?.isVisible = false
}
}
fun bind(activeCallView: CurrentCallsView, interactionListener: CurrentCallsView.Callback) {
this.currentCallsView = activeCallView
this.currentCallsView?.callback = interactionListener
this.currentCall?.addListener(tickListener)
}
fun unBind() {
this.currentCallsView?.callback = null
this.currentCall?.removeListener(tickListener)
currentCallsView = null
}
}

View File

@ -37,7 +37,8 @@ import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.CurrentCallsCardView
import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.CurrentCallsViewPresenter
import im.vector.app.core.ui.views.KeysBackupBanner
import im.vector.app.databinding.FragmentHomeDetailBinding
import im.vector.app.features.call.SharedKnownCallsViewModel
@ -56,7 +57,6 @@ import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.BannerState
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import im.vector.app.features.workers.signout.ServerBackupStatusViewState
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
@ -69,12 +69,11 @@ class HomeDetailFragment @Inject constructor(
private val colorProvider: ColorProvider,
private val alertManager: PopupAlertManager,
private val callManager: WebRtcCallManager,
private val vectorPreferences: VectorPreferences,
private val session: Session
private val vectorPreferences: VectorPreferences
) : VectorBaseFragment<FragmentHomeDetailBinding>(),
KeysBackupBanner.Delegate,
ServerBackupStatusViewModel.Factory,
CurrentCallsCardView.Callback {
CurrentCallsView.Callback,
ServerBackupStatusViewModel.Factory {
private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
@ -117,6 +116,8 @@ class HomeDetailFragment @Inject constructor(
return FragmentHomeDetailBinding.inflate(inflater, container, false)
}
private val currentCallsViewPresenter = CurrentCallsViewPresenter()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
@ -188,11 +189,16 @@ class HomeDetailFragment @Inject constructor(
sharedCallActionViewModel
.liveKnownCalls
.observe(viewLifecycleOwner, {
views.currentCallsCardView.render(callManager.getCurrentCall(), callManager.getCalls())
currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), callManager.getCalls())
invalidateOptionsMenu()
})
}
override fun onDestroyView() {
currentCallsViewPresenter.unBind()
super.onDestroyView()
}
override fun onResume() {
super.onResume()
// update notification tab if needed
@ -289,11 +295,7 @@ class HomeDetailFragment @Inject constructor(
}
private fun setupActiveCallView() {
views.currentCallsCardView.apply {
this.avatarRenderer = this@HomeDetailFragment.avatarRenderer
this.session = this@HomeDetailFragment.session
this.callback = this@HomeDetailFragment
}
currentCallsViewPresenter.bind(views.currentCallsView, this)
}
private fun setupToolbar() {

View File

@ -89,7 +89,9 @@ import im.vector.app.core.intent.getMimeTypeFromUri
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.showOptimizedSnackbar
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.CurrentCallsCardView
import im.vector.app.core.ui.views.CurrentCallsViewPresenter
import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.FailedMessagesWarningView
import im.vector.app.core.ui.views.NotificationAreaView
import im.vector.app.core.utils.Debouncer
@ -240,7 +242,7 @@ class RoomDetailFragment @Inject constructor(
AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback,
GalleryOrCameraDialogHelper.Listener,
CurrentCallsCardView.Callback {
CurrentCallsView.Callback {
companion object {
/**
@ -299,6 +301,7 @@ class RoomDetailFragment @Inject constructor(
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
private var lockSendButton = false
private val currentCallsViewPresenter = CurrentCallsViewPresenter()
private lateinit var emojiPopup: EmojiPopup
@ -346,7 +349,7 @@ class RoomDetailFragment @Inject constructor(
knownCallsViewModel
.liveKnownCalls
.observe(viewLifecycleOwner, {
views.currentCallsCardView.render(callManager.getCurrentCall(), it)
currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), it)
invalidateOptionsMenu()
})
@ -685,6 +688,7 @@ class RoomDetailFragment @Inject constructor(
override fun onDestroyView() {
timelineEventController.callback = null
timelineEventController.removeModelBuildListener(modelBuildListener)
currentCallsViewPresenter.unBind()
modelBuildListener = null
autoCompleter.clear()
debouncer.cancelAll()
@ -730,11 +734,7 @@ class RoomDetailFragment @Inject constructor(
}
private fun setupActiveCallView() {
views.currentCallsCardView.apply {
this.callback = this@RoomDetailFragment
this.avatarRenderer = this@RoomDetailFragment.avatarRenderer
this.session = this@RoomDetailFragment.session
}
currentCallsViewPresenter.bind(views.currentCallsView, this)
}
private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) {

View File

@ -11,6 +11,15 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<im.vector.app.core.ui.views.CurrentCallsView
android:id="@+id/currentCallsView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/homeKeysBackupBanner"
tools:visibility="visible" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/groupToolbar"
android:layout_width="match_parent"
@ -135,21 +144,6 @@
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintTop_toBottomOf="@+id/homeKeysBackupBanner" />
<im.vector.app.core.ui.views.CurrentCallsCardView
android:id="@+id/currentCallsCardView"
android:layout_width="@dimen/call_pip_width"
android:layout_height="@dimen/call_pip_height"
app:cardElevation="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardCornerRadius="@dimen/call_pip_radius"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="0dp"

View File

@ -12,6 +12,14 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<im.vector.app.core.ui.views.CurrentCallsView
android:id="@+id/currentCallsView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/roomToolbar"
android:layout_width="match_parent"
@ -104,10 +112,11 @@
android:id="@+id/removeJitsiWidgetView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
android:background="?android:colorBackground"
android:minHeight="54dp"
app:layout_constraintTop_toBottomOf="@id/syncStateView"/>
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/syncStateView" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/timelineRecyclerView"
@ -123,14 +132,14 @@
<com.google.android.material.chip.Chip
android:id="@+id/jumpToReadMarkerView"
style="?vctr_jump_to_unread_style"
app:chipIcon="@drawable/ic_jump_to_unread"
app:closeIcon="@drawable/ic_close_24dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="24dp"
android:text="@string/room_jump_to_first_unread"
android:visibility="invisible"
app:chipIcon="@drawable/ic_jump_to_unread"
app:closeIcon="@drawable/ic_close_24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"
@ -186,21 +195,6 @@
app:barrierDirection="top"
app:constraint_referenced_ids="composerLayout,notificationAreaView, failedMessagesWarningView" />
<im.vector.app.core.ui.views.CurrentCallsCardView
android:id="@+id/currentCallsCardView"
android:layout_width="@dimen/call_pip_width"
android:layout_height="@dimen/call_pip_height"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
app:cardElevation="8dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardCornerRadius="@dimen/call_pip_radius"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/failedMessagesWarningView"/>
<im.vector.app.core.platform.BadgeFloatingActionButton
android:id="@+id/jumpToBottomView"
android:layout_width="wrap_content"

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:foreground="?attr/selectableItemBackground"
tools:parentTag="android.widget.FrameLayout">
<TextView
android:id="@+id/currentCallsInfo"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:gravity="center"
android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="16dp"
android:paddingBottom="12dp"
android:text="@string/call_only_active"
android:textColor="?colorOnPrimary" />
</merge>

View File

@ -1,93 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="@dimen/call_pip_width"
android:layout_height="@dimen/call_pip_height"
android:backgroundTint="@color/bg_call_screen_blur"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="@dimen/call_pip_radius"
tools:parentTag="com.google.android.material.card.MaterialCardView">
<org.webrtc.SurfaceViewRenderer
android:id="@+id/activeCallPiP"
android:layout_width="@dimen/call_pip_width"
android:layout_height="@dimen/call_pip_height"
android:visibility="gone"
tools:visibility="invisible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/avatarViews"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/activeCallOpponentAvatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:importantForAccessibility="no"
android:scaleType="centerCrop"
app:layout_constraintBottom_toTopOf="@+id/otherCallOpponentAvatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/activeCallPausedIcon"
android:layout_width="16dp"
android:layout_height="16dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_call_small_pause"
app:layout_constraintBottom_toBottomOf="@+id/activeCallOpponentAvatar"
app:layout_constraintEnd_toEndOf="@+id/activeCallOpponentAvatar"
app:layout_constraintStart_toStartOf="@+id/activeCallOpponentAvatar"
app:layout_constraintTop_toTopOf="@+id/activeCallOpponentAvatar" />
<ImageView
android:id="@+id/otherCallOpponentAvatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:importantForAccessibility="no"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/activeCallOpponentAvatar"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/otherCallPausedIcon"
android:layout_width="8dp"
android:layout_height="8dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_call_small_pause"
app:layout_constraintBottom_toBottomOf="@+id/otherCallOpponentAvatar"
app:layout_constraintEnd_toEndOf="@+id/otherCallOpponentAvatar"
app:layout_constraintStart_toStartOf="@+id/otherCallOpponentAvatar"
app:layout_constraintTop_toTopOf="@+id/otherCallOpponentAvatar" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/activeCallProgress"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:indeterminateTint="@color/element_background_light"
android:indeterminateTintMode="src_atop"
android:visibility="gone"
tools:visibility="visible" />
</merge>