VoIP : branch dialpad in transfer call screen

This commit is contained in:
ganfra 2021-01-19 10:46:43 +01:00
parent 55fd983fd3
commit 0f77c5be90
11 changed files with 168 additions and 83 deletions

View file

@ -29,4 +29,4 @@ data class CallCapabilities(
@Json(name = "m.call.transferee") val transferee: Boolean? = null @Json(name = "m.call.transferee") val transferee: Boolean? = null
) )
fun CallCapabilities?.supportCallTransfer() = this?.transferee.orFalse() fun CallCapabilities?.supportCallTransfer() = true//this?.transferee.orFalse()

View file

@ -20,10 +20,11 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.createdirect.DirectRoomHelper
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject
class DialPadLookup(val session: Session, class DialPadLookup @Inject constructor(val session: Session,
val directRoomHelper: DirectRoomHelper, val directRoomHelper: DirectRoomHelper,
val callManager: WebRtcCallManager val callManager: WebRtcCallManager
) { ) {
class Failure : Throwable() class Failure : Throwable()

View file

@ -19,5 +19,6 @@ package im.vector.app.features.call.transfer
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed class CallTransferAction : VectorViewModelAction { sealed class CallTransferAction : VectorViewModelAction {
data class Connect(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction() data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction()
data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferAction()
} }

View file

@ -20,26 +20,17 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.widget.Toast import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import com.google.android.material.tabs.TabLayoutMediator
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.checkPermissions
import im.vector.app.databinding.ActivityCallTransferBinding import im.vector.app.databinding.ActivityCallTransferBinding
import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.contactsbook.ContactsBookViewState import im.vector.app.features.contactsbook.ContactsBookViewState
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel import im.vector.app.features.userdirectory.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel import im.vector.app.features.userdirectory.UserListViewModel
import im.vector.app.features.userdirectory.UserListViewState import im.vector.app.features.userdirectory.UserListViewState
@ -56,16 +47,19 @@ class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>(),
UserListViewModel.Factory, UserListViewModel.Factory,
ContactsBookViewModel.Factory { ContactsBookViewModel.Factory {
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
@Inject lateinit var userListViewModelFactory: UserListViewModel.Factory @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
@Inject lateinit var callTransferViewModelFactory: CallTransferViewModel.Factory @Inject lateinit var callTransferViewModelFactory: CallTransferViewModel.Factory
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
private lateinit var sectionsPagerAdapter: CallTransferPagerAdapter
private val callTransferViewModel: CallTransferViewModel by viewModel() private val callTransferViewModel: CallTransferViewModel by viewModel()
override fun getBinding() = ActivityCallTransferBinding.inflate(layoutInflater) override fun getBinding() = ActivityCallTransferBinding.inflate(layoutInflater)
override fun getCoordinatorLayout() = views.vectorCoordinatorLayout
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector) super.injectWith(injector)
injector.inject(this) injector.inject(this)
@ -86,39 +80,28 @@ class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>(),
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
waitingView = views.waitingView.waitingView waitingView = views.waitingView.waitingView
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
sharedActionViewModel
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
UserListSharedAction.OpenPhoneBook -> openPhoneBook()
// not exhaustive because it's a sharedAction
else -> {
}
}
}
.disposeOnDestroy()
if (isFirstCreation()) {
addFragment(
R.id.callTransferFragmentContainer,
UserListFragment::class.java,
UserListFragmentArgs(
title = "",
menuResId = -1,
singleSelection = true,
showInviteActions = false,
showToolbar = false
),
USER_LIST_FRAGMENT_TAG
)
}
callTransferViewModel.observeViewEvents { callTransferViewModel.observeViewEvents {
when (it) { when (it) {
is CallTransferViewEvents.Dismiss -> finish() is CallTransferViewEvents.Dismiss -> finish()
CallTransferViewEvents.Loading -> showWaitingView() CallTransferViewEvents.Loading -> showWaitingView()
is CallTransferViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure)) is CallTransferViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
} }
} }
sectionsPagerAdapter = CallTransferPagerAdapter(this).register()
views.callTransferViewPager.adapter = sectionsPagerAdapter
sectionsPagerAdapter.onDialPadOkClicked = { phoneNumber ->
val action = CallTransferAction.ConnectWithPhoneNumber(views.callTransferConsultCheckBox.isChecked, phoneNumber)
callTransferViewModel.handle(action)
}
TabLayoutMediator(views.callTransferTabLayout, views.callTransferViewPager) { tab, position ->
when (position) {
0 -> tab.text = getString(R.string.call_transfer_users_tab_title)
1 -> tab.text = getString(R.string.call_dial_pad_title)
}
}.attach()
configureToolbar(views.callTransferToolbar) configureToolbar(views.callTransferToolbar)
views.callTransferToolbar.title = getString(R.string.call_transfer_title) views.callTransferToolbar.title = getString(R.string.call_transfer_title)
setupConnectAction() setupConnectAction()
@ -126,36 +109,14 @@ class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>(),
private fun setupConnectAction() { private fun setupConnectAction() {
views.callTransferConnectAction.debouncedClicks { views.callTransferConnectAction.debouncedClicks {
val userListFragment = supportFragmentManager.findFragmentByTag(USER_LIST_FRAGMENT_TAG) as? UserListFragment val selectedUser = sectionsPagerAdapter.userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull()
val selectedUser = userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull()
if (selectedUser != null) { if (selectedUser != null) {
val action = CallTransferAction.Connect(views.callTransferConsultCheckBox.isChecked, selectedUser) val action = CallTransferAction.ConnectWithUserId(views.callTransferConsultCheckBox.isChecked, selectedUser)
callTransferViewModel.handle(action) callTransferViewModel.handle(action)
} }
} }
} }
private fun openPhoneBook() {
// Check permission first
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
this,
PERMISSION_REQUEST_CODE_READ_CONTACTS,
0)) {
addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
doOnPostResume { addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java) }
}
} else {
Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
}
}
companion object { companion object {
fun newIntent(context: Context, callId: String): Intent { fun newIntent(context: Context, callId: String): Intent {

View file

@ -0,0 +1,90 @@
/*
* 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.features.call.transfer
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.platform.Restorable
import im.vector.app.features.call.dialpad.DialPadFragment
import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs
class CallTransferPagerAdapter(
private val fragmentActivity: FragmentActivity
) : FragmentStateAdapter(fragmentActivity), Restorable{
val userListFragment: UserListFragment?
get() = findFragmentAtPosition(0) as? UserListFragment
val dialPadFragment: DialPadFragment?
get() = findFragmentAtPosition(1) as? DialPadFragment
var onDialPadOkClicked: ((String) -> Unit)? = null
override fun getItemCount() = 2
override fun createFragment(position: Int): Fragment {
val fragment: Fragment
if (position == 0) {
fragment = fragmentActivity.supportFragmentManager.fragmentFactory.instantiate(fragmentActivity.classLoader, UserListFragment::class.java.name)
fragment.arguments = UserListFragmentArgs(
title = "",
menuResId = -1,
singleSelection = true,
showInviteActions = false,
showToolbar = false,
showContactBookAction = false
).toMvRxBundle()
} else {
fragment = fragmentActivity.supportFragmentManager.fragmentFactory.instantiate(fragmentActivity.classLoader, DialPadFragment::class.java.name)
(fragment as DialPadFragment).apply {
arguments = Bundle().apply {
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
}
applyCallback()
}
}
return fragment
}
private fun findFragmentAtPosition(position: Int): Fragment? {
return fragmentActivity.supportFragmentManager.findFragmentByTag("f$position")
}
override fun onSaveInstanceState(outState: Bundle) = Unit
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
dialPadFragment?.applyCallback()
}
private fun DialPadFragment.applyCallback(): DialPadFragment{
callback = object : DialPadFragment.Callback {
override fun onOkClicked(formatted: String?, raw: String?) {
if (raw.isNullOrEmpty()) return
onDialPadOkClicked?.invoke(raw)
}
}
return this
}
}

View file

@ -24,6 +24,7 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -31,6 +32,7 @@ import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState, class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
private val dialPadLookup: DialPadLookup,
callManager: WebRtcCallManager) callManager: WebRtcCallManager)
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) { : VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
@ -72,11 +74,12 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: CallTransferAction) { override fun handle(action: CallTransferAction) {
when (action) { when (action) {
is CallTransferAction.Connect -> transferCall(action) is CallTransferAction.ConnectWithUserId -> connectWithUserId(action)
is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action)
}.exhaustive }.exhaustive
} }
private fun transferCall(action: CallTransferAction.Connect) { private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
viewModelScope.launch { viewModelScope.launch {
try { try {
_viewEvents.post(CallTransferViewEvents.Loading) _viewEvents.post(CallTransferViewEvents.Loading)
@ -87,4 +90,18 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
} }
} }
} }
private fun connectWithPhoneNumber(action: CallTransferAction.ConnectWithPhoneNumber) {
viewModelScope.launch {
try {
_viewEvents.post(CallTransferViewEvents.Loading)
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
call?.mxCall?.transfer(result.userId, result.roomId)
_viewEvents.post(CallTransferViewEvents.Dismiss)
} catch (failure: Throwable) {
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
}
}
}
} }

View file

@ -64,13 +64,15 @@ class UserListController @Inject constructor(private val session: Session,
}) })
} }
} }
actionItem { if(currentState.showContactBookAction) {
id(R.drawable.ic_baseline_perm_contact_calendar_24) actionItem {
title(stringProvider.getString(R.string.contacts_book_title)) id(R.drawable.ic_baseline_perm_contact_calendar_24)
actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24) title(stringProvider.getString(R.string.contacts_book_title))
clickAction(View.OnClickListener { actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24)
callback?.onContactBookClick() clickAction(View.OnClickListener {
}) callback?.onContactBookClick()
})
}
} }
if (currentState.showInviteActions()) { if (currentState.showInviteActions()) {
actionItem { actionItem {

View file

@ -26,5 +26,6 @@ data class UserListFragmentArgs(
val excludedUserIds: Set<String>? = null, val excludedUserIds: Set<String>? = null,
val singleSelection: Boolean = false, val singleSelection: Boolean = false,
val showInviteActions: Boolean = true, val showInviteActions: Boolean = true,
val showContactBookAction: Boolean = true,
val showToolbar: Boolean = true val showToolbar: Boolean = true
) : Parcelable ) : Parcelable

View file

@ -31,13 +31,15 @@ data class UserListViewState(
val pendingSelections: Set<PendingSelection> = emptySet(), val pendingSelections: Set<PendingSelection> = emptySet(),
val searchTerm: String = "", val searchTerm: String = "",
val singleSelection: Boolean, val singleSelection: Boolean,
private val showInviteActions: Boolean private val showInviteActions: Boolean,
val showContactBookAction: Boolean
) : MvRxState { ) : MvRxState {
constructor(args: UserListFragmentArgs) : this( constructor(args: UserListFragmentArgs) : this(
excludedUserIds = args.excludedUserIds, excludedUserIds = args.excludedUserIds,
singleSelection = args.singleSelection, singleSelection = args.singleSelection,
showInviteActions = args.showInviteActions showInviteActions = args.showInviteActions,
showContactBookAction = args.showContactBookAction
) )
fun getSelectedMatrixId(): List<String> { fun getSelectedMatrixId(): List<String> {

View file

@ -17,14 +17,24 @@
android:elevation="4dp" android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView <com.google.android.material.tabs.TabLayout
android:id="@+id/callTransferFragmentContainer" android:id="@+id/callTransferTabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/callTransferToolbar"
app:tabGravity="fill"
app:tabMaxWidth="0dp"
app:tabMode="fixed" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/callTransferViewPager"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/callTransferActionsLayout" app:layout_constraintBottom_toTopOf="@+id/callTransferActionsLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/callTransferToolbar" /> app:layout_constraintTop_toBottomOf="@id/callTransferTabLayout"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<RelativeLayout <RelativeLayout
android:background="?riotx_header_panel_background" android:background="?riotx_header_panel_background"
@ -67,7 +77,6 @@
</RelativeLayout> </RelativeLayout>
<include <include
android:id="@+id/waiting_view" android:id="@+id/waiting_view"
layout="@layout/merge_overlay_waiting_view" /> layout="@layout/merge_overlay_waiting_view" />

View file

@ -2795,6 +2795,7 @@
<string name="call_transfer_connect_action">Connect</string> <string name="call_transfer_connect_action">Connect</string>
<string name="call_transfer_title">Transfer</string> <string name="call_transfer_title">Transfer</string>
<string name="call_transfer_failure">An error occurred while transferring call</string> <string name="call_transfer_failure">An error occurred while transferring call</string>
<string name="call_transfer_users_tab_title">Users</string>
</resources> </resources>