Calling signout multi sessions use case in main screen for other sessions

This commit is contained in:
Maxime NATUREL 2022-10-24 17:52:13 +02:00
parent 1bda54323a
commit 0f8e5919da
4 changed files with 150 additions and 10 deletions

View file

@ -20,6 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
sealed class DevicesAction : VectorViewModelAction {
// ReAuth
object SsoAuthDone : DevicesAction()
data class PasswordAuthDone(val password: String) : DevicesAction()
object ReAuthCancelled : DevicesAction()
// Others
object VerifyCurrentSession : DevicesAction()
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
object MultiSignoutOtherSessions : DevicesAction()

View file

@ -17,17 +17,21 @@
package im.vector.app.features.settings.devices.v2
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
sealed class DevicesViewEvent : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : DevicesViewEvent()
data class Failure(val throwable: Throwable) : DevicesViewEvent()
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvent()
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvent()
data class RequestReAuth(
val registrationFlowResponse: RegistrationFlowResponse,
val lastErrorCode: String?
) : DevicesViewEvent()
data class ShowVerifyDevice(val userId: String, val transactionId: String?) : DevicesViewEvent()
object SelfVerification : DevicesViewEvent()
data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent()
object PromptResetSecrets : DevicesViewEvent()
object SignoutSuccess : DevicesViewEvent()
data class SignoutError(val error: Throwable) : DevicesViewEvent()
}

View file

@ -21,24 +21,42 @@ import com.airbnb.mvrx.Success
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.auth.PendingAuthHandler
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber
import javax.net.ssl.HttpsURLConnection
import kotlin.coroutines.Continuation
class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState,
activeSessionHolder: ActiveSessionHolder,
private val stringProvider: StringProvider,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
private val signoutSessionsUseCase: SignoutSessionsUseCase,
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
private val pendingAuthHandler: PendingAuthHandler,
refreshDevicesUseCase: RefreshDevicesUseCase,
) : VectorSessionsListViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState, activeSessionHolder, refreshDevicesUseCase) {
@ -98,6 +116,9 @@ class DevicesViewModel @AssistedInject constructor(
// TODO update unit tests
override fun handle(action: DevicesAction) {
when (action) {
is DevicesAction.PasswordAuthDone -> handlePasswordAuthDone(action)
DevicesAction.ReAuthCancelled -> handleReAuthCancelled()
DevicesAction.SsoAuthDone -> handleSsoAuthDone()
is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction()
is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction()
DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions()
@ -119,7 +140,79 @@ class DevicesViewModel @AssistedInject constructor(
// TODO implement when needed
}
private fun handleMultiSignoutOtherSessions() {
// TODO call multi signout use case with all other devices than the current one
private fun handleMultiSignoutOtherSessions() = withState { state ->
viewModelScope.launch {
setLoading(true)
val deviceIds = getDeviceIdsOfOtherSessions(state)
if (deviceIds.isEmpty()) {
return@launch
}
val signoutResult = signout(deviceIds)
setLoading(false)
if (signoutResult.isSuccess) {
onSignoutSuccess()
} else {
when (val failure = signoutResult.exceptionOrNull()) {
null -> onSignoutSuccess()
else -> onSignoutFailure(failure)
}
}
}
}
private fun getDeviceIdsOfOtherSessions(state: DevicesViewState): List<String> {
val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId
return state.devices()
?.mapNotNull { fullInfo -> fullInfo.deviceInfo.deviceId.takeUnless { it == currentDeviceId } }
.orEmpty()
}
private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) {
is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result)
is SignoutSessionResult.Completed -> Unit
}
}
})
private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) {
Timber.d("onReAuthNeeded")
pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation
_viewEvents.post(DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode))
}
private fun setLoading(isLoading: Boolean) {
setState { copy(isLoading = isLoading) }
}
private fun onSignoutSuccess() {
Timber.d("signout success")
refreshDeviceList()
_viewEvents.post(DevicesViewEvent.SignoutSuccess)
}
private fun onSignoutFailure(failure: Throwable) {
Timber.e("signout failure", failure)
val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
stringProvider.getString(R.string.authentication_error)
} else {
stringProvider.getString(R.string.matrix_error)
}
_viewEvents.post(DevicesViewEvent.SignoutError(Exception(failureMessage)))
}
private fun handleSsoAuthDone() {
pendingAuthHandler.ssoAuthDone()
}
private fun handlePasswordAuthDone(action: DevicesAction.PasswordAuthDone) {
pendingAuthHandler.passwordAuthDone(action.password)
}
private fun handleReAuthCancelled() {
pendingAuthHandler.reAuthCancelled()
}
}

View file

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices.v2
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
@ -30,6 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.dialogs.ManuallyVerifyDialog
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.setTextColor
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
@ -37,6 +39,7 @@ import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.databinding.FragmentSettingsDevicesBinding
import im.vector.app.features.VectorFeatures
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.login.qr.QrCodeLoginArgs
@ -48,6 +51,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import javax.inject.Inject
@ -102,10 +106,7 @@ class VectorSettingsDevicesFragment :
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is DevicesViewEvent.Loading -> showLoading(it.message)
is DevicesViewEvent.Failure -> showFailure(it.throwable)
is DevicesViewEvent.RequestReAuth -> Unit // TODO. Next PR
is DevicesViewEvent.PromptRenameDevice -> Unit // TODO. Next PR
is DevicesViewEvent.RequestReAuth -> askForReAuthentication(it)
is DevicesViewEvent.ShowVerifyDevice -> {
VerificationBottomSheet.withArgs(
roomId = null,
@ -124,6 +125,8 @@ class VectorSettingsDevicesFragment :
is DevicesViewEvent.PromptResetSecrets -> {
navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
}
is DevicesViewEvent.SignoutError -> showFailure(it.error)
is DevicesViewEvent.SignoutSuccess -> Unit // do nothing
}
}
}
@ -137,6 +140,7 @@ class VectorSettingsDevicesFragment :
views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.otherSessionsHeaderMultiSignout -> {
// TODO ask for confirmation
viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
true
}
@ -366,4 +370,37 @@ class VectorSettingsDevicesFragment :
excludeCurrentDevice = true
)
}
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
LoginFlowTypes.SSO -> {
viewModel.handle(DevicesAction.SsoAuthDone)
}
LoginFlowTypes.PASSWORD -> {
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
viewModel.handle(DevicesAction.PasswordAuthDone(password))
}
else -> {
viewModel.handle(DevicesAction.ReAuthCancelled)
}
}
} else {
viewModel.handle(DevicesAction.ReAuthCancelled)
}
}
/**
* Launch the re auth activity to get credentials.
*/
private fun askForReAuthentication(reAuthReq: DevicesViewEvent.RequestReAuth) {
ReAuthActivity.newIntent(
requireContext(),
reAuthReq.registrationFlowResponse,
reAuthReq.lastErrorCode,
getString(R.string.devices_delete_dialog_title)
).let { intent ->
reAuthActivityResultLauncher.launch(intent)
}
}
}