Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
97cfc7d
Adding changelog entry
Oct 20, 2022
f45cc71
Adding new menu entry for multi signout
Oct 20, 2022
1ed92e5
Adding overflow menu capability in sessions list header view
Oct 20, 2022
ae4a728
Handling press on multi signout action in other sessions list screen
Oct 20, 2022
810c93c
Handling press on multi signout action from header menu in other sess…
Oct 20, 2022
7e836c0
Updating the action title to include sessions number
Oct 24, 2022
bb262f0
Adding new "delete_devices" request API
Oct 24, 2022
1bda543
Calling signout multi sessions use case in other sessions screen
Oct 24, 2022
0f8e591
Calling signout multi sessions use case in main screen for other sess…
Oct 24, 2022
727c746
Adding confirmation dialog before signout process
Oct 25, 2022
a968ac0
Adding unit tests for signout sessions use case
Oct 25, 2022
5bcf2ac
Adding unit tests for other sessions list view model
Oct 25, 2022
880ee40
Adding unit tests about reAuth actions for devices view model
Oct 25, 2022
a3df90a
Adding unit tests about multi signout action for devices view model
Oct 25, 2022
e0d511a
Fixing a name of a mocked component
Oct 25, 2022
4b0b335
Fixing code quality issues
Oct 25, 2022
76e2b6b
Removing some TODOs
Oct 25, 2022
db42d1c
Fix post rebase unit tests
Oct 26, 2022
ef5aaf7
Fix forbidden usage of AlertDialog
Oct 26, 2022
d2d9da3
Exclude the current session from other sessions and security recommen…
Oct 26, 2022
3c7ba85
Removing unused string
Oct 26, 2022
5515cd3
Use SHOW_AS_ACTION_IF_ROOM tag
Oct 26, 2022
1d2b8e7
Adding min size annotation to task params
Nov 7, 2022
45050e8
Removing error formatting from ViewModel
Nov 7, 2022
6d26208
Moving UI auth interceptor into use case
Nov 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7418.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Session manager] Multi-session signout
5 changes: 5 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3345,6 +3345,11 @@
<string name="device_manager_other_sessions_no_inactive_sessions_found">No inactive sessions found.</string>
<string name="device_manager_other_sessions_clear_filter">Clear Filter</string>
<string name="device_manager_other_sessions_select">Select sessions</string>
<string name="device_manager_other_sessions_multi_signout_selection">Sign out</string>
<plurals name="device_manager_other_sessions_multi_signout_all">
<item quantity="one">Sign out of %1$d session</item>
<item quantity="other">Sign out of %1$d sessions</item>
</plurals>
<string name="device_manager_session_overview_signout">Sign out of this session</string>
<string name="device_manager_session_details_title">Session details</string>
<string name="device_manager_session_details_description">Application, device, and activity information.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<attr name="sessionsListHeaderTitle" format="string" />
<attr name="sessionsListHeaderDescription" format="string" />
<attr name="sessionsListHeaderHasLearnMoreLink" format="boolean" />
<attr name="sessionsListHeaderMenu" format="reference" />
</declare-styleable>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.crypto

import android.content.Context
import androidx.annotation.Size
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback
Expand Down Expand Up @@ -55,6 +56,8 @@ interface CryptoService {

fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)

fun deleteDevices(@Size(min = 1) deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)

fun getCryptoVersion(context: Context, longFormat: Boolean): String

fun isCryptoEnabled(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,12 @@ internal class DefaultCryptoService @Inject constructor(
}

override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback)
}

override fun deleteDevices(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
deleteDeviceTask
.configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
.configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.api
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams
import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
Expand Down Expand Up @@ -136,6 +137,17 @@ internal interface CryptoApi {
@Body params: DeleteDeviceParams
)

/**
* Deletes the given devices, and invalidates any access token associated with them.
* Doc: https://spec.matrix.org/v1.4/client-server-api/#post_matrixclientv3delete_devices
*
* @param params the deletion parameters
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_V3 + "delete_devices")
suspend fun deleteDevices(
@Body params: DeleteDevicesParams
)

/**
* Update the device information.
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import com.squareup.moshi.JsonClass
*/
@JsonClass(generateAdapter = true)
internal data class DeleteDeviceParams(
/**
* Additional authentication information for the user-interactive authentication API.
*/
@Json(name = "auth")
val auth: Map<String, *>? = null
val auth: Map<String, *>? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.crypto.model.rest

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* This class provides the parameter to delete several devices.
*/
@JsonClass(generateAdapter = true)
internal data class DeleteDevicesParams(
/**
* Additional authentication information for the user-interactive authentication API.
*/
@Json(name = "auth")
val auth: Map<String, *>? = null,

/**
* Required: The list of device IDs to delete.
*/
@Json(name = "devices")
val deviceIds: List<String>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

package org.matrix.android.sdk.internal.crypto.tasks

import androidx.annotation.Size
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.uia.UiaResult
import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
Expand All @@ -30,7 +32,7 @@ import javax.inject.Inject

internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
data class Params(
val deviceId: String,
@Size(min = 1) val deviceIds: List<String>,
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
val userAuthParam: UIABaseAuth?
)
Expand All @@ -42,9 +44,24 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
) : DeleteDeviceTask {

override suspend fun execute(params: DeleteDeviceTask.Params) {
require(params.deviceIds.isNotEmpty())

try {
executeRequest(globalErrorReceiver) {
cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap()))
val userAuthParam = params.userAuthParam?.asMap()
if (params.deviceIds.size == 1) {
cryptoApi.deleteDevice(
deviceId = params.deviceIds.first(),
DeleteDeviceParams(auth = userAuthParam)
)
} else {
cryptoApi.deleteDevices(
DeleteDevicesParams(
auth = userAuthParam,
deviceIds = params.deviceIds
)
)
}
}
} catch (throwable: Throwable) {
if (params.userInteractiveAuthInterceptor == null ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal object NetworkConstants {
const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/"
const val URI_API_PREFIX_PATH_V3 = "$URI_API_PREFIX_PATH/v3/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"

// Media
Expand Down
29 changes: 29 additions & 0 deletions vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.core.extensions

import android.view.MenuItem
import androidx.annotation.ColorInt
import androidx.core.text.toSpannable
import im.vector.app.core.utils.colorizeMatchingText

fun MenuItem.setTextColor(@ColorInt color: Int) {
val currentTitle = title.orEmpty().toString()
title = currentTitle
.toSpannable()
.colorizeMatchingText(currentTitle, color)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ 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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ package im.vector.app.features.settings.devices.v2
import im.vector.app.core.platform.VectorViewEvents
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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@ import dagger.assisted.AssistedInject
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.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.SignoutSessionsReAuthNeeded
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.extensions.orFalse
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber

class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState,
Expand All @@ -39,6 +45,9 @@ class DevicesViewModel @AssistedInject constructor(
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) {

Expand Down Expand Up @@ -97,8 +106,12 @@ class DevicesViewModel @AssistedInject constructor(

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()
}
}

Expand All @@ -116,4 +129,66 @@ class DevicesViewModel @AssistedInject constructor(
private fun handleMarkAsManuallyVerifiedAction() {
// TODO implement when needed
}

private fun handleMultiSignoutOtherSessions() = withState { state ->
viewModelScope.launch {
setLoading(true)
val deviceIds = getDeviceIdsOfOtherSessions(state)
if (deviceIds.isEmpty()) {
return@launch
}
val result = signout(deviceIds)
setLoading(false)

val error = result.exceptionOrNull()
if (error == null) {
onSignoutSuccess()
} else {
onSignoutFailure(error)
}
}
}

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, this::onReAuthNeeded)

private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
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)
_viewEvents.post(DevicesViewEvent.SignoutError(failure))
}

private fun handleSsoAuthDone() {
pendingAuthHandler.ssoAuthDone()
}

private fun handlePasswordAuthDone(action: DevicesAction.PasswordAuthDone) {
pendingAuthHandler.passwordAuthDone(action.password)
}

private fun handleReAuthCancelled() {
pendingAuthHandler.reAuthCancelled()
}
}
Loading