Calling signout multi sessions use case in other sessions screen

This commit is contained in:
Maxime NATUREL 2022-10-24 16:51:22 +02:00
parent bb262f0c41
commit 1bda54323a
7 changed files with 207 additions and 6 deletions

View file

@ -20,6 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
sealed class OtherSessionsAction : VectorViewModelAction {
// ReAuth
object SsoAuthDone : OtherSessionsAction()
data class PasswordAuthDone(val password: String) : OtherSessionsAction()
object ReAuthCancelled : OtherSessionsAction()
// Others
data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction()
data class EnableSelectMode(val deviceId: String?) : OtherSessionsAction()
object DisableSelectMode : OtherSessionsAction()

View file

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices.v2.othersessions
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
@ -32,6 +33,7 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.setTextColor
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK
@ -40,6 +42,7 @@ import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.databinding.FragmentOtherSessionsBinding
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
@ -47,6 +50,7 @@ import im.vector.app.features.settings.devices.v2.list.OtherSessionsView
import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject
@ -158,8 +162,9 @@ class OtherSessionsFragment :
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is OtherSessionsViewEvents.Loading -> showLoading(it.message)
is OtherSessionsViewEvents.Failure -> showFailure(it.throwable)
is OtherSessionsViewEvents.SignoutError -> showFailure(it.error)
is OtherSessionsViewEvents.RequestReAuth -> askForReAuthentication(it)
OtherSessionsViewEvents.SignoutSuccess -> enableSelectMode(false)
}
}
}
@ -191,6 +196,7 @@ class OtherSessionsFragment :
}
override fun invalidate() = withState(viewModel) { state ->
updateLoading(state.isLoading)
if (state.devices is Success) {
val devices = state.devices.invoke()
renderDevices(devices, state.currentFilter)
@ -198,6 +204,14 @@ class OtherSessionsFragment :
}
}
private fun updateLoading(isLoading: Boolean) {
if (isLoading) {
showLoading(null)
} else {
dismissLoadingDialog()
}
}
private fun updateToolbar(devices: List<DeviceFullInfo>, isSelectModeEnabled: Boolean) {
invalidateOptionsMenu()
val title = if (isSelectModeEnabled) {
@ -312,4 +326,37 @@ class OtherSessionsFragment :
override fun onViewAllOtherSessionsClicked() {
// NOOP. We don't have this button in this screen
}
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
LoginFlowTypes.SSO -> {
viewModel.handle(OtherSessionsAction.SsoAuthDone)
}
LoginFlowTypes.PASSWORD -> {
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
viewModel.handle(OtherSessionsAction.PasswordAuthDone(password))
}
else -> {
viewModel.handle(OtherSessionsAction.ReAuthCancelled)
}
}
} else {
viewModel.handle(OtherSessionsAction.ReAuthCancelled)
}
}
/**
* Launch the re auth activity to get credentials.
*/
private fun askForReAuthentication(reAuthReq: OtherSessionsViewEvents.RequestReAuth) {
ReAuthActivity.newIntent(
requireContext(),
reAuthReq.registrationFlowResponse,
reAuthReq.lastErrorCode,
getString(R.string.devices_delete_dialog_title)
).let { intent ->
reAuthActivityResultLauncher.launch(intent)
}
}
}

View file

@ -17,8 +17,14 @@
package im.vector.app.features.settings.devices.v2.othersessions
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
sealed class OtherSessionsViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents()
data class Failure(val throwable: Throwable) : OtherSessionsViewEvents()
data class RequestReAuth(
val registrationFlowResponse: RegistrationFlowResponse,
val lastErrorCode: String?
) : OtherSessionsViewEvents()
object SignoutSuccess : OtherSessionsViewEvents()
data class SignoutError(val error: Throwable) : OtherSessionsViewEvents()
}

View file

@ -21,19 +21,38 @@ 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.GetDeviceFullInfoListUseCase
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
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 kotlinx.coroutines.Job
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.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 OtherSessionsViewModel @AssistedInject constructor(
@Assisted private val initialState: OtherSessionsViewState,
activeSessionHolder: ActiveSessionHolder,
private val stringProvider: StringProvider,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val signoutSessionsUseCase: SignoutSessionsUseCase,
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
private val pendingAuthHandler: PendingAuthHandler,
refreshDevicesUseCase: RefreshDevicesUseCase
) : VectorSessionsListViewModel<OtherSessionsViewState, OtherSessionsAction, OtherSessionsViewEvents>(
initialState, activeSessionHolder, refreshDevicesUseCase
@ -68,6 +87,9 @@ class OtherSessionsViewModel @AssistedInject constructor(
// TODO update unit tests
override fun handle(action: OtherSessionsAction) {
when (action) {
is OtherSessionsAction.PasswordAuthDone -> handlePasswordAuthDone(action)
OtherSessionsAction.ReAuthCancelled -> handleReAuthCancelled()
OtherSessionsAction.SsoAuthDone -> handleSsoAuthDone()
is OtherSessionsAction.FilterDevices -> handleFilterDevices(action)
OtherSessionsAction.DisableSelectMode -> handleDisableSelectMode()
is OtherSessionsAction.EnableSelectMode -> handleEnableSelectMode(action.deviceId)
@ -145,7 +167,80 @@ class OtherSessionsViewModel @AssistedInject constructor(
}
}
private fun handleMultiSignout() {
// TODO call multi signout use case with all or only selected devices depending on the ViewState
private fun handleMultiSignout() = withState { state ->
viewModelScope.launch {
setLoading(true)
val deviceIds = getDeviceIdsToSignout(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 getDeviceIdsToSignout(state: OtherSessionsViewState): List<String> {
return if (state.isSelectModeEnabled) {
state.devices()?.filter { it.isSelected }.orEmpty()
} else {
state.devices().orEmpty()
}.mapNotNull { it.deviceInfo.deviceId }
}
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(OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode))
}
private fun setLoading(isLoading: Boolean) {
setState { copy(isLoading = isLoading) }
}
private fun onSignoutSuccess() {
Timber.d("signout success")
refreshDeviceList()
_viewEvents.post(OtherSessionsViewEvents.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(OtherSessionsViewEvents.SignoutError(Exception(failureMessage)))
}
private fun handleSsoAuthDone() {
pendingAuthHandler.ssoAuthDone()
}
private fun handlePasswordAuthDone(action: OtherSessionsAction.PasswordAuthDone) {
pendingAuthHandler.passwordAuthDone(action.password)
}
private fun handleReAuthCancelled() {
pendingAuthHandler.reAuthCancelled()
}
}

View file

@ -27,6 +27,7 @@ data class OtherSessionsViewState(
val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS,
val excludeCurrentDevice: Boolean = false,
val isSelectModeEnabled: Boolean = false,
val isLoading: Boolean = false,
) : MavericksState {
constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice)

View file

@ -21,6 +21,9 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.util.awaitCallback
import javax.inject.Inject
/**
* Use case to signout a single session.
*/
class SignoutSessionUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
) {

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2022 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.settings.devices.v2.signout
import im.vector.app.core.di.ActiveSessionHolder
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.util.awaitCallback
import javax.inject.Inject
/**
* Use case to signout several sessions.
*/
class SignoutSessionsUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
) {
// TODO add unit tests
suspend fun execute(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result<Unit> {
return deleteDevices(deviceIds, userInteractiveAuthInterceptor)
}
private suspend fun deleteDevices(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching {
awaitCallback { matrixCallback ->
activeSessionHolder.getActiveSession()
.cryptoService()
.deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback)
}
}
}