VoIP: fix hangup when multiple calls + clean after Benoit review
This commit is contained in:
parent
e0cd210852
commit
97b9064d29
@ -147,7 +147,7 @@ interface ScreenComponent {
|
||||
fun inject(activity: VectorJitsiActivity)
|
||||
fun inject(activity: SearchActivity)
|
||||
fun inject(activity: UserCodeActivity)
|
||||
fun inject(callTransferActivity: CallTransferActivity)
|
||||
fun inject(activity: CallTransferActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
@ -38,7 +38,7 @@ import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.HomeRoomListDataSource
|
||||
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.VectorHtmlCompressor
|
||||
import im.vector.app.features.login.ReAuthHelper
|
||||
@ -159,7 +159,7 @@ interface VectorComponent {
|
||||
|
||||
fun webRtcCallManager(): WebRtcCallManager
|
||||
|
||||
fun roomSummaryHolder(): RoomSummaryHolder
|
||||
fun roomSummaryHolder(): RoomSummariesHolder
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
@ -52,19 +52,14 @@ class CurrentCallsView @JvmOverloads constructor(
|
||||
val heldCalls = connectedCalls.filter {
|
||||
it.isLocalOnHold || it.remoteOnHold
|
||||
}
|
||||
if (connectedCalls.size == 1) {
|
||||
if (heldCalls.size == 1) {
|
||||
views.currentCallsInfo.setText(R.string.call_only_paused)
|
||||
} else {
|
||||
views.currentCallsInfo.text = resources.getString(R.string.call_only_active, formattedDuration)
|
||||
}
|
||||
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.size > 1) {
|
||||
views.currentCallsInfo.text = resources.getString(R.string.call_only_multiple_paused, heldCalls.size)
|
||||
} else if (heldCalls.size == 1) {
|
||||
views.currentCallsInfo.text = resources.getString(R.string.call_active_and_single_paused, formattedDuration)
|
||||
if (heldCalls.isEmpty()) {
|
||||
resources.getString(R.string.call_only_active, formattedDuration)
|
||||
} else {
|
||||
views.currentCallsInfo.text = resources.getString(R.string.call_active_and_multiple_paused, formattedDuration, heldCalls.size)
|
||||
resources.getQuantityString(R.plurals.call_one_active_and_other_paused, heldCalls.size, formattedDuration, heldCalls.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
@ -30,7 +31,7 @@ import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
|
||||
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
|
||||
private val callManager: WebRtcCallManager)
|
||||
callManager: WebRtcCallManager)
|
||||
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@ -47,7 +48,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
|
||||
}
|
||||
}
|
||||
|
||||
private var call: WebRtcCall? = null
|
||||
private val call = callManager.getCallById(initialState.callId)
|
||||
private val callListener = object : WebRtcCall.Listener {
|
||||
override fun onStateUpdate(call: MxCall) {
|
||||
if (call.state == CallState.Terminated) {
|
||||
@ -57,12 +58,10 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
|
||||
}
|
||||
|
||||
init {
|
||||
val webRtcCall = callManager.getCallById(initialState.callId)
|
||||
if (webRtcCall == null) {
|
||||
if (call == null) {
|
||||
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||
} else {
|
||||
call = webRtcCall
|
||||
webRtcCall.addListener(callListener)
|
||||
call.addListener(callListener)
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,7 +73,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
|
||||
override fun handle(action: CallTransferAction) {
|
||||
when (action) {
|
||||
is CallTransferAction.Connect -> transferCall(action)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun transferCall(action: CallTransferAction.Connect) {
|
||||
|
@ -718,18 +718,18 @@ class WebRtcCall(val mxCall: MxCall,
|
||||
val cameraManager = context.getSystemService<CameraManager>()!!
|
||||
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
|
||||
}
|
||||
val wasRinging = mxCall.state is CallState.LocalRinging
|
||||
mxCall.state = CallState.Terminated
|
||||
GlobalScope.launch(dispatcher) {
|
||||
release()
|
||||
}
|
||||
onCallEnded(this)
|
||||
if (originatedByMe) {
|
||||
if (mxCall.state is CallState.LocalRinging) {
|
||||
if (wasRinging) {
|
||||
mxCall.reject()
|
||||
} else {
|
||||
mxCall.hangUp(reason)
|
||||
}
|
||||
} else {
|
||||
mxCall.state = CallState.Terminated
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,7 +136,7 @@ class WebRtcCallManager @Inject constructor(
|
||||
|
||||
fun headSetButtonTapped() {
|
||||
Timber.v("## VOIP headSetButtonTapped")
|
||||
val call = currentCall.get() ?: return
|
||||
val call = getCurrentCall() ?: return
|
||||
if (call.mxCall.state is CallState.LocalRinging) {
|
||||
// accept call
|
||||
call.acceptIncomingCall()
|
||||
@ -178,7 +178,7 @@ class WebRtcCallManager @Inject constructor(
|
||||
|
||||
private fun onCallActive(call: WebRtcCall) {
|
||||
Timber.v("## VOIP WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}")
|
||||
val currentCall = currentCall.get().takeIf { it != call }
|
||||
val currentCall = getCurrentCall().takeIf { it != call }
|
||||
currentCall?.updateRemoteOnHold(onHold = true)
|
||||
this.currentCall.setAndNotify(call)
|
||||
}
|
||||
@ -189,13 +189,13 @@ class WebRtcCallManager @Inject constructor(
|
||||
callAudioManager.stop()
|
||||
callsByCallId.remove(call.mxCall.callId)
|
||||
callsByRoomId[call.mxCall.roomId]?.remove(call)
|
||||
if (currentCall.get() == call) {
|
||||
if (getCurrentCall() == call) {
|
||||
val otherCall = getCalls().lastOrNull()
|
||||
currentCall.setAndNotify(otherCall)
|
||||
}
|
||||
// This must be done in this thread
|
||||
executor.execute {
|
||||
if (currentCall.get() == null) {
|
||||
if (getCurrentCall() == null) {
|
||||
Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one")
|
||||
peerConnectionFactory?.dispose()
|
||||
peerConnectionFactory = null
|
||||
@ -210,7 +210,7 @@ class WebRtcCallManager @Inject constructor(
|
||||
Timber.w("## VOIP you already have a call in this room")
|
||||
return
|
||||
}
|
||||
if (currentCall.get() != null && currentCall.get()?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) {
|
||||
if (getCurrentCall() != null && getCurrentCall()?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) {
|
||||
Timber.w("## VOIP cannot start outgoing call")
|
||||
// Just ignore, maybe we could answer from other session?
|
||||
return
|
||||
@ -218,7 +218,7 @@ class WebRtcCallManager @Inject constructor(
|
||||
executor.execute {
|
||||
createPeerConnectionFactoryIfNeeded()
|
||||
}
|
||||
currentCall.get()?.updateRemoteOnHold(onHold = true)
|
||||
getCurrentCall()?.updateRemoteOnHold(onHold = true)
|
||||
val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
|
||||
val webRtcCall = createWebRtcCall(mxCall)
|
||||
currentCall.setAndNotify(webRtcCall)
|
||||
@ -268,7 +268,7 @@ class WebRtcCallManager @Inject constructor(
|
||||
|
||||
fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||
Timber.v("## VOIP onWiredDeviceEvent $event")
|
||||
currentCall.get() ?: return
|
||||
getCurrentCall() ?: return
|
||||
// sometimes we received un-wanted unplugged...
|
||||
callAudioManager.wiredStateChange(event)
|
||||
}
|
||||
@ -284,7 +284,7 @@ class WebRtcCallManager @Inject constructor(
|
||||
Timber.w("## VOIP you already have a call in this room")
|
||||
return
|
||||
}
|
||||
if ((currentCall.get() != null && currentCall.get()?.mxCall?.state !is CallState.Connected) || getCalls().size >= 2) {
|
||||
if ((getCurrentCall() != null && getCurrentCall()?.mxCall?.state !is CallState.Connected) || getCalls().size >= 2) {
|
||||
Timber.w("## VOIP receiving incoming call but cannot handle it")
|
||||
// Just ignore, maybe we could answer from other session?
|
||||
return
|
||||
|
@ -38,7 +38,7 @@ import im.vector.app.features.command.ParsedCommand
|
||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
||||
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.home.room.typing.TypingHelper
|
||||
@ -111,7 +111,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
private val rawService: RawService,
|
||||
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
|
||||
private val stickerPickerActionHandler: StickerPickerActionHandler,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val typingHelper: TypingHelper,
|
||||
private val callManager: WebRtcCallManager,
|
||||
private val chatEffectManager: ChatEffectManager,
|
||||
@ -1376,7 +1376,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
|
||||
private fun observeSummaryState() {
|
||||
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
||||
roomSummaryHolder.set(summary)
|
||||
roomSummariesHolder.set(summary)
|
||||
setState {
|
||||
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
|
||||
copy(typingMessage = typingMessage)
|
||||
@ -1421,7 +1421,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
roomSummaryHolder.remove(room.roomId)
|
||||
roomSummariesHolder.remove(room.roomId)
|
||||
timeline.dispose()
|
||||
timeline.removeAllListeners()
|
||||
if (vectorPreferences.sendTypingNotifs()) {
|
||||
|
@ -22,7 +22,7 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
@ -43,7 +43,7 @@ class CallItemFactory @Inject constructor(
|
||||
private val messageInformationDataFactory: MessageInformationDataFactory,
|
||||
private val messageItemAttributesFactory: MessageItemAttributesFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val callManager: WebRtcCallManager
|
||||
) {
|
||||
|
||||
@ -135,7 +135,7 @@ class CallItemFactory @Inject constructor(
|
||||
isStillActive: Boolean,
|
||||
callback: TimelineEventController.Callback?
|
||||
): CallTileTimelineItem? {
|
||||
val userOfInterest = roomSummaryHolder.get(roomId)?.toMatrixItem() ?: return null
|
||||
val userOfInterest = roomSummariesHolder.get(roomId)?.toMatrixItem() ?: return null
|
||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback).let {
|
||||
CallTileTimelineItem.Attributes(
|
||||
callId = callId,
|
||||
|
@ -22,7 +22,7 @@ import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.prevSameTypeEvents
|
||||
@ -47,7 +47,7 @@ import javax.inject.Inject
|
||||
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val roomSummaryHolder: RoomSummaryHolder) {
|
||||
private val roomSummariesHolder: RoomSummariesHolder) {
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<Long>()
|
||||
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
||||
@ -77,7 +77,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDirectRoom(roomId: String) = roomSummaryHolder.get(roomId)?.isDirect.orFalse()
|
||||
private fun isDirectRoom(roomId: String) = roomSummariesHolder.get(roomId)?.isDirect.orFalse()
|
||||
|
||||
private fun buildMembershipEventsMergedSummary(currentPosition: Int,
|
||||
items: List<TimelineEvent>,
|
||||
@ -208,7 +208,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||
readReceiptsCallback = callback,
|
||||
callback = callback,
|
||||
currentUserId = currentUserId,
|
||||
roomSummary = roomSummaryHolder.get(event.roomId),
|
||||
roomSummary = roomSummariesHolder.get(event.roomId),
|
||||
canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
|
||||
canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
|
||||
canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
|
||||
|
@ -20,7 +20,7 @@ import im.vector.app.core.epoxy.EmptyItem_
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import timber.log.Timber
|
||||
@ -32,7 +32,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
private val defaultItemFactory: DefaultItemFactory,
|
||||
private val encryptionItemFactory: EncryptionItemFactory,
|
||||
private val roomCreateItemFactory: RoomCreateItemFactory,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val verificationConclusionItemFactory: VerificationItemFactory,
|
||||
private val callItemFactory: CallItemFactory,
|
||||
private val userPreferencesProvider: UserPreferencesProvider) {
|
||||
|
@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.format
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.extensions.appendNl
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
@ -56,7 +56,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||
private val activeSessionDataSource: ActiveSessionDataSource,
|
||||
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val sp: StringProvider
|
||||
) {
|
||||
|
||||
@ -68,7 +68,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||
private fun RoomSummary?.isDm() = this?.isDirect.orFalse()
|
||||
|
||||
fun format(timelineEvent: TimelineEvent): CharSequence? {
|
||||
val rs = roomSummaryHolder.get(timelineEvent.roomId)
|
||||
val rs = roomSummariesHolder.get(timelineEvent.roomId)
|
||||
return when (val type = timelineEvent.root.getClearType()) {
|
||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, rs)
|
||||
|
@ -45,7 +45,7 @@ import javax.inject.Inject
|
||||
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
|
||||
*/
|
||||
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
|
||||
@ -116,7 +116,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
}
|
||||
|
||||
private fun getE2EDecoration(event: TimelineEvent): E2EDecoration {
|
||||
val roomSummary = roomSummaryHolder.get(event.roomId)
|
||||
val roomSummary = roomSummariesHolder.get(event.roomId)
|
||||
return if (
|
||||
event.root.sendState == SendState.SYNCED
|
||||
&& roomSummary?.isEncrypted.orFalse()
|
||||
|
@ -25,7 +25,7 @@ import javax.inject.Singleton
|
||||
You should probably use this only in the context of the timeline
|
||||
*/
|
||||
@Singleton
|
||||
class RoomSummaryHolder @Inject constructor() {
|
||||
class RoomSummariesHolder @Inject constructor() {
|
||||
|
||||
private var roomSummaries = HashMap<String, RoomSummary>()
|
||||
|
@ -30,12 +30,12 @@ data class UserListViewState(
|
||||
val filteredMappedContacts: List<MappedContact> = emptyList(),
|
||||
val pendingSelections: Set<PendingSelection> = emptySet(),
|
||||
val searchTerm: String = "",
|
||||
val myUserId: String = "",
|
||||
val singleSelection: Boolean,
|
||||
private val showInviteActions: Boolean
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: UserListFragmentArgs) : this(
|
||||
excludedUserIds = args.excludedUserIds,
|
||||
singleSelection = args.singleSelection,
|
||||
showInviteActions = args.showInviteActions
|
||||
)
|
||||
|
@ -20,7 +20,7 @@
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:textSize="14sp"
|
||||
android:text="@string/call_active_and_single_paused"
|
||||
android:text="@string/call_only_active"
|
||||
android:textColor="@color/white"
|
||||
app:drawableTint="@color/white"
|
||||
app:drawableStartCompat="@drawable/ic_call_answer" />
|
||||
|
@ -2777,10 +2777,15 @@
|
||||
|
||||
|
||||
<string name="call_only_active">Active call (%1$s)</string>
|
||||
<string name="call_only_paused">Paused call</string>
|
||||
<string name="call_active_and_single_paused">1 active call (%1$s) · 1 paused call</string>
|
||||
<string name="call_active_and_multiple_paused">1 active call (%1$s) · %2$d paused calls</string>
|
||||
<string name="call_only_multiple_paused">%1$d paused calls</string>
|
||||
<plurals name="call_only_paused">
|
||||
<item quantity="one">Paused call</item>
|
||||
<item quantity="other">%1$d paused calls</item>
|
||||
</plurals>
|
||||
<plurals name="call_one_active_and_other_paused">
|
||||
<item quantity="one">1 active call (%1$s) · 1 paused call</item>
|
||||
<item quantity="other">1 active call (%1$s) · %2$d paused calls</item>
|
||||
</plurals>
|
||||
|
||||
|
||||
<string name="call_transfer_consult_first">Consult first</string>
|
||||
<string name="call_transfer_connect_action">Connect</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user