mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 20:10:04 +03:00
Merge branch 'develop' into feature/ons/fix_device_manager_verified_desc
This commit is contained in:
commit
b2589a1e4d
52 changed files with 1354 additions and 415 deletions
|
@ -60,8 +60,8 @@ jobs:
|
||||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
query: |
|
query: |
|
||||||
mutation add_to_project($projectid:ID!, $contentid:ID!) {
|
mutation add_to_project($projectid:ID!, $contentid:ID!) {
|
||||||
addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
|
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
|
||||||
projectNextItem {
|
item {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,8 +129,8 @@ jobs:
|
||||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
query: |
|
query: |
|
||||||
mutation add_to_project($projectid:ID!, $contentid:ID!) {
|
mutation add_to_project($projectid:ID!, $contentid:ID!) {
|
||||||
addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
|
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
|
||||||
projectNextItem {
|
item {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
Changes in Element v1.5.7 (2022-11-07)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Fix regression when syncing with homeserver < 1.4. ([#7534](https://github.com/vector-im/element-android/issues/7534))
|
||||||
|
|
||||||
Changes in Element v1.5.6 (2022-11-02)
|
Changes in Element v1.5.6 (2022-11-02)
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
|
|
1
changelog.d/7418.feature
Normal file
1
changelog.d/7418.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[Session manager] Multi-session signout
|
1
changelog.d/7501.bugfix
Normal file
1
changelog.d/7501.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix duplicated mention pills in some cases
|
1
changelog.d/7509.bugfix
Normal file
1
changelog.d/7509.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
When joining a room, the message composer is displayed once the room is loaded.
|
|
@ -26,7 +26,7 @@ def jjwt = "0.11.5"
|
||||||
// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
|
// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
|
||||||
// the whole commit which set version 0.16.0-SNAPSHOT
|
// the whole commit which set version 0.16.0-SNAPSHOT
|
||||||
def vanniktechEmoji = "0.16.0-SNAPSHOT"
|
def vanniktechEmoji = "0.16.0-SNAPSHOT"
|
||||||
def sentry = "6.6.0"
|
def sentry = "6.7.0"
|
||||||
def fragment = "1.5.4"
|
def fragment = "1.5.4"
|
||||||
// Testing
|
// Testing
|
||||||
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
|
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
|
||||||
|
|
2
fastlane/metadata/android/en-US/changelogs/40105070.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40105070.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: new UI for selecting an attachment.
|
||||||
|
Full changelog: https://github.com/vector-im/element-android/releases
|
|
@ -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_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_clear_filter">Clear Filter</string>
|
||||||
<string name="device_manager_other_sessions_select">Select sessions</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_overview_signout">Sign out of this session</string>
|
||||||
<string name="device_manager_session_details_title">Session details</string>
|
<string name="device_manager_session_details_title">Session details</string>
|
||||||
<string name="device_manager_session_details_description">Application, device, and activity information.</string>
|
<string name="device_manager_session_details_description">Application, device, and activity information.</string>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<attr name="sessionsListHeaderTitle" format="string" />
|
<attr name="sessionsListHeaderTitle" format="string" />
|
||||||
<attr name="sessionsListHeaderDescription" format="string" />
|
<attr name="sessionsListHeaderDescription" format="string" />
|
||||||
<attr name="sessionsListHeaderHasLearnMoreLink" format="boolean" />
|
<attr name="sessionsListHeaderHasLearnMoreLink" format="boolean" />
|
||||||
|
<attr name="sessionsListHeaderMenu" format="reference" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.session.search
|
package org.matrix.android.sdk.session.search
|
||||||
|
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -43,7 +44,7 @@ class SearchMessagesTest : InstrumentedTest {
|
||||||
cryptoTestData.firstSession
|
cryptoTestData.firstSession
|
||||||
.searchService()
|
.searchService()
|
||||||
.search(
|
.search(
|
||||||
searchTerm = "lore",
|
searchTerm = "lorem",
|
||||||
limit = 10,
|
limit = 10,
|
||||||
includeProfile = true,
|
includeProfile = true,
|
||||||
afterLimit = 0,
|
afterLimit = 0,
|
||||||
|
@ -61,7 +62,7 @@ class SearchMessagesTest : InstrumentedTest {
|
||||||
cryptoTestData.firstSession
|
cryptoTestData.firstSession
|
||||||
.searchService()
|
.searchService()
|
||||||
.search(
|
.search(
|
||||||
searchTerm = "lore",
|
searchTerm = "lorem",
|
||||||
roomId = cryptoTestData.roomId,
|
roomId = cryptoTestData.roomId,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
includeProfile = true,
|
includeProfile = true,
|
||||||
|
@ -73,7 +74,28 @@ class SearchMessagesTest : InstrumentedTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doTest(block: suspend (CryptoTestData) -> SearchResult) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
@Test
|
||||||
|
fun sendTextMessageAndSearchPartOfItIncompleteWord() {
|
||||||
|
doTest(expectedNumberOfResult = 0) { cryptoTestData ->
|
||||||
|
cryptoTestData.firstSession
|
||||||
|
.searchService()
|
||||||
|
.search(
|
||||||
|
searchTerm = "lore", /* incomplete word */
|
||||||
|
roomId = cryptoTestData.roomId,
|
||||||
|
limit = 10,
|
||||||
|
includeProfile = true,
|
||||||
|
afterLimit = 0,
|
||||||
|
beforeLimit = 10,
|
||||||
|
orderByRecent = true,
|
||||||
|
nextBatch = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doTest(
|
||||||
|
expectedNumberOfResult: Int = 2,
|
||||||
|
block: suspend (CryptoTestData) -> SearchResult,
|
||||||
|
) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val aliceRoomId = cryptoTestData.roomId
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
@ -87,7 +109,7 @@ class SearchMessagesTest : InstrumentedTest {
|
||||||
|
|
||||||
val data = block.invoke(cryptoTestData)
|
val data = block.invoke(cryptoTestData)
|
||||||
|
|
||||||
assertTrue(data.results?.size == 2)
|
data.results?.size shouldBeEqualTo expectedNumberOfResult
|
||||||
assertTrue(
|
assertTrue(
|
||||||
data.results
|
data.results
|
||||||
?.all {
|
?.all {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.matrix.android.sdk.api.session.crypto
|
package org.matrix.android.sdk.api.session.crypto
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.Size
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
@ -55,6 +56,8 @@ interface CryptoService {
|
||||||
|
|
||||||
fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)
|
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 getCryptoVersion(context: Context, longFormat: Boolean): String
|
||||||
|
|
||||||
fun isCryptoEnabled(): Boolean
|
fun isCryptoEnabled(): Boolean
|
||||||
|
|
|
@ -242,8 +242,12 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
|
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
|
deleteDeviceTask
|
||||||
.configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
|
.configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) {
|
||||||
this.executionThread = TaskThread.CRYPTO
|
this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.DeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
|
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.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.KeyChangesResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody
|
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
|
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
|
||||||
|
@ -136,6 +137,17 @@ internal interface CryptoApi {
|
||||||
@Body params: DeleteDeviceParams
|
@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.
|
* Update the device information.
|
||||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid
|
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid
|
||||||
|
|
|
@ -23,6 +23,9 @@ import com.squareup.moshi.JsonClass
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class DeleteDeviceParams(
|
internal data class DeleteDeviceParams(
|
||||||
|
/**
|
||||||
|
* Additional authentication information for the user-interactive authentication API.
|
||||||
|
*/
|
||||||
@Json(name = "auth")
|
@Json(name = "auth")
|
||||||
val auth: Map<String, *>? = null
|
val auth: Map<String, *>? = null,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>,
|
||||||
|
)
|
|
@ -16,12 +16,14 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
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.UIABaseAuth
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.session.uia.UiaResult
|
import org.matrix.android.sdk.api.session.uia.UiaResult
|
||||||
import org.matrix.android.sdk.internal.auth.registration.handleUIA
|
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.api.CryptoApi
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
|
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.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
@ -30,7 +32,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
|
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val deviceId: String,
|
@Size(min = 1) val deviceIds: List<String>,
|
||||||
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
|
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
|
||||||
val userAuthParam: UIABaseAuth?
|
val userAuthParam: UIABaseAuth?
|
||||||
)
|
)
|
||||||
|
@ -42,9 +44,24 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
|
||||||
) : DeleteDeviceTask {
|
) : DeleteDeviceTask {
|
||||||
|
|
||||||
override suspend fun execute(params: DeleteDeviceTask.Params) {
|
override suspend fun execute(params: DeleteDeviceTask.Params) {
|
||||||
|
require(params.deviceIds.isNotEmpty())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
executeRequest(globalErrorReceiver) {
|
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) {
|
} catch (throwable: Throwable) {
|
||||||
if (params.userInteractiveAuthInterceptor == null ||
|
if (params.userInteractiveAuthInterceptor == null ||
|
||||||
|
|
|
@ -22,6 +22,7 @@ internal object NetworkConstants {
|
||||||
const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
|
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_R0 = "$URI_API_PREFIX_PATH/r0/"
|
||||||
const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/"
|
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/"
|
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
|
||||||
|
|
||||||
// Media
|
// Media
|
||||||
|
|
|
@ -67,7 +67,9 @@ internal object FilterFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createElementTimelineFilter(): RoomEventFilter? {
|
private fun createElementTimelineFilter(): RoomEventFilter? {
|
||||||
return RoomEventFilter(enableUnreadThreadNotifications = true)
|
// we need to check if homeserver supports thread notifications before setting this param
|
||||||
|
// return RoomEventFilter(enableUnreadThreadNotifications = true)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createElementStateFilter(): RoomEventFilter {
|
private fun createElementStateFilter(): RoomEventFilter {
|
||||||
|
|
|
@ -132,7 +132,7 @@ dependencies {
|
||||||
implementation libs.androidx.biometric
|
implementation libs.androidx.biometric
|
||||||
|
|
||||||
api "org.threeten:threetenbp:1.4.0:no-tzdb"
|
api "org.threeten:threetenbp:1.4.0:no-tzdb"
|
||||||
api "com.gabrielittner.threetenbp:lazythreetenbp:0.11.0"
|
api "com.gabrielittner.threetenbp:lazythreetenbp:0.12.0"
|
||||||
|
|
||||||
implementation libs.squareup.moshi
|
implementation libs.squareup.moshi
|
||||||
kapt libs.squareup.moshiKotlin
|
kapt libs.squareup.moshiKotlin
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -24,26 +24,6 @@ package im.vector.app.features.analytics.plan
|
||||||
* definition. These properties must all be device independent.
|
* definition. These properties must all be device independent.
|
||||||
*/
|
*/
|
||||||
data class UserProperties(
|
data class UserProperties(
|
||||||
/**
|
|
||||||
* Whether the user has the favourites space enabled.
|
|
||||||
*/
|
|
||||||
val webMetaSpaceFavouritesEnabled: Boolean? = null,
|
|
||||||
/**
|
|
||||||
* Whether the user has the home space set to all rooms.
|
|
||||||
*/
|
|
||||||
val webMetaSpaceHomeAllRooms: Boolean? = null,
|
|
||||||
/**
|
|
||||||
* Whether the user has the home space enabled.
|
|
||||||
*/
|
|
||||||
val webMetaSpaceHomeEnabled: Boolean? = null,
|
|
||||||
/**
|
|
||||||
* Whether the user has the other rooms space enabled.
|
|
||||||
*/
|
|
||||||
val webMetaSpaceOrphansEnabled: Boolean? = null,
|
|
||||||
/**
|
|
||||||
* Whether the user has the people space enabled.
|
|
||||||
*/
|
|
||||||
val webMetaSpacePeopleEnabled: Boolean? = null,
|
|
||||||
/**
|
/**
|
||||||
* The active filter in the All Chats screen.
|
* The active filter in the All Chats screen.
|
||||||
*/
|
*/
|
||||||
|
@ -109,11 +89,6 @@ data class UserProperties(
|
||||||
|
|
||||||
fun getProperties(): Map<String, Any>? {
|
fun getProperties(): Map<String, Any>? {
|
||||||
return mutableMapOf<String, Any>().apply {
|
return mutableMapOf<String, Any>().apply {
|
||||||
webMetaSpaceFavouritesEnabled?.let { put("WebMetaSpaceFavouritesEnabled", it) }
|
|
||||||
webMetaSpaceHomeAllRooms?.let { put("WebMetaSpaceHomeAllRooms", it) }
|
|
||||||
webMetaSpaceHomeEnabled?.let { put("WebMetaSpaceHomeEnabled", it) }
|
|
||||||
webMetaSpaceOrphansEnabled?.let { put("WebMetaSpaceOrphansEnabled", it) }
|
|
||||||
webMetaSpacePeopleEnabled?.let { put("WebMetaSpacePeopleEnabled", it) }
|
|
||||||
allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) }
|
allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) }
|
||||||
ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) }
|
ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) }
|
||||||
numFavouriteRooms?.let { put("numFavouriteRooms", it) }
|
numFavouriteRooms?.let { put("numFavouriteRooms", it) }
|
||||||
|
|
|
@ -1169,6 +1169,9 @@ class TimelineFragment :
|
||||||
lazyLoadedViews.inviteView(false)?.isVisible = false
|
lazyLoadedViews.inviteView(false)?.isVisible = false
|
||||||
|
|
||||||
if (mainState.tombstoneEvent == null) {
|
if (mainState.tombstoneEvent == null) {
|
||||||
|
views.composerContainer.isInvisible = !messageComposerState.isComposerVisible
|
||||||
|
views.voiceMessageRecorderContainer.isVisible = messageComposerState.isVoiceMessageRecorderVisible
|
||||||
|
|
||||||
when (messageComposerState.canSendMessage) {
|
when (messageComposerState.canSendMessage) {
|
||||||
CanSendStatus.Allowed -> {
|
CanSendStatus.Allowed -> {
|
||||||
NotificationAreaView.State.Hidden
|
NotificationAreaView.State.Hidden
|
||||||
|
@ -1224,6 +1227,7 @@ class TimelineFragment :
|
||||||
|
|
||||||
private fun FragmentTimelineBinding.hideComposerViews() {
|
private fun FragmentTimelineBinding.hideComposerViews() {
|
||||||
composerContainer.isVisible = false
|
composerContainer.isVisible = false
|
||||||
|
voiceMessageRecorderContainer.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) {
|
private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) {
|
||||||
|
|
|
@ -83,6 +83,20 @@ class PillsPostProcessor @AssistedInject constructor(
|
||||||
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
|
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
|
||||||
val startSpan = renderedText.getSpanStart(linkSpan)
|
val startSpan = renderedText.getSpanStart(linkSpan)
|
||||||
val endSpan = renderedText.getSpanEnd(linkSpan)
|
val endSpan = renderedText.getSpanEnd(linkSpan)
|
||||||
|
// GlideImagesPlugin causes duplicated pills if we have a nested spans in the pill span,
|
||||||
|
// such as images or italic text.
|
||||||
|
// Accordingly, it's better to remove all spans that are contained in this span before rendering.
|
||||||
|
renderedText.getSpans(startSpan, endSpan, Any::class.java).forEach remove@{
|
||||||
|
if (it !is LinkSpan) {
|
||||||
|
// Make sure to only remove spans that are contained in this link, and not are bigger than this link, e.g. like reply-blocks
|
||||||
|
val start = renderedText.getSpanStart(it)
|
||||||
|
if (start < startSpan) return@remove
|
||||||
|
val end = renderedText.getSpanEnd(it)
|
||||||
|
if (end > endSpan) return@remove
|
||||||
|
|
||||||
|
renderedText.removeSpan(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
addPillSpan(renderedText, pillSpan, startSpan, endSpan)
|
addPillSpan(renderedText, pillSpan, startSpan, endSpan)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,13 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
|
|
||||||
sealed class DevicesAction : VectorViewModelAction {
|
sealed class DevicesAction : VectorViewModelAction {
|
||||||
|
// ReAuth
|
||||||
|
object SsoAuthDone : DevicesAction()
|
||||||
|
data class PasswordAuthDone(val password: String) : DevicesAction()
|
||||||
|
object ReAuthCancelled : DevicesAction()
|
||||||
|
|
||||||
|
// Others
|
||||||
object VerifyCurrentSession : DevicesAction()
|
object VerifyCurrentSession : DevicesAction()
|
||||||
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
|
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
|
||||||
|
object MultiSignoutOtherSessions : DevicesAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,17 @@ package im.vector.app.features.settings.devices.v2
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
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.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
|
||||||
|
|
||||||
sealed class DevicesViewEvent : VectorViewEvents {
|
sealed class DevicesViewEvent : VectorViewEvents {
|
||||||
data class Loading(val message: CharSequence? = null) : DevicesViewEvent()
|
data class RequestReAuth(
|
||||||
data class Failure(val throwable: Throwable) : DevicesViewEvent()
|
val registrationFlowResponse: RegistrationFlowResponse,
|
||||||
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvent()
|
val lastErrorCode: String?
|
||||||
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvent()
|
) : DevicesViewEvent()
|
||||||
|
|
||||||
data class ShowVerifyDevice(val userId: String, val transactionId: String?) : DevicesViewEvent()
|
data class ShowVerifyDevice(val userId: String, val transactionId: String?) : DevicesViewEvent()
|
||||||
object SelfVerification : DevicesViewEvent()
|
object SelfVerification : DevicesViewEvent()
|
||||||
data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent()
|
data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent()
|
||||||
object PromptResetSecrets : DevicesViewEvent()
|
object PromptResetSecrets : DevicesViewEvent()
|
||||||
|
object SignoutSuccess : DevicesViewEvent()
|
||||||
|
data class SignoutError(val error: Throwable) : DevicesViewEvent()
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,13 +24,19 @@ import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
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.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.CheckIfCurrentSessionCanBeVerifiedUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
|
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
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(
|
class DevicesViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: DevicesViewState,
|
@Assisted initialState: DevicesViewState,
|
||||||
|
@ -39,6 +45,9 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
|
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
|
||||||
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
|
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
|
||||||
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
|
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
|
||||||
|
private val signoutSessionsUseCase: SignoutSessionsUseCase,
|
||||||
|
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
|
||||||
|
private val pendingAuthHandler: PendingAuthHandler,
|
||||||
refreshDevicesUseCase: RefreshDevicesUseCase,
|
refreshDevicesUseCase: RefreshDevicesUseCase,
|
||||||
) : VectorSessionsListViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState, activeSessionHolder, refreshDevicesUseCase) {
|
) : VectorSessionsListViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState, activeSessionHolder, refreshDevicesUseCase) {
|
||||||
|
|
||||||
|
@ -97,8 +106,12 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
override fun handle(action: DevicesAction) {
|
override fun handle(action: DevicesAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
is DevicesAction.PasswordAuthDone -> handlePasswordAuthDone(action)
|
||||||
|
DevicesAction.ReAuthCancelled -> handleReAuthCancelled()
|
||||||
|
DevicesAction.SsoAuthDone -> handleSsoAuthDone()
|
||||||
is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction()
|
is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction()
|
||||||
is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction()
|
is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction()
|
||||||
|
DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,4 +129,66 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
private fun handleMarkAsManuallyVerifiedAction() {
|
private fun handleMarkAsManuallyVerifiedAction() {
|
||||||
// TODO implement when needed
|
// 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.settings.devices.v2
|
package im.vector.app.features.settings.devices.v2
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -30,12 +31,15 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.date.VectorDateFormatter
|
import im.vector.app.core.date.VectorDateFormatter
|
||||||
import im.vector.app.core.dialogs.ManuallyVerifyDialog
|
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.platform.VectorBaseFragment
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.DrawableProvider
|
import im.vector.app.core.resources.DrawableProvider
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.databinding.FragmentSettingsDevicesBinding
|
import im.vector.app.databinding.FragmentSettingsDevicesBinding
|
||||||
import im.vector.app.features.VectorFeatures
|
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.recover.SetupMode
|
||||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||||
import im.vector.app.features.login.qr.QrCodeLoginArgs
|
import im.vector.app.features.login.qr.QrCodeLoginArgs
|
||||||
|
@ -47,6 +51,8 @@ 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.SecurityRecommendationView
|
||||||
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
|
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
|
||||||
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
|
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
|
||||||
|
import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -70,6 +76,8 @@ class VectorSettingsDevicesFragment :
|
||||||
|
|
||||||
@Inject lateinit var stringProvider: StringProvider
|
@Inject lateinit var stringProvider: StringProvider
|
||||||
|
|
||||||
|
@Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase
|
||||||
|
|
||||||
private val viewModel: DevicesViewModel by fragmentViewModel()
|
private val viewModel: DevicesViewModel by fragmentViewModel()
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSettingsDevicesBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSettingsDevicesBinding {
|
||||||
|
@ -91,6 +99,7 @@ class VectorSettingsDevicesFragment :
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
initWaitingView()
|
initWaitingView()
|
||||||
|
initOtherSessionsHeaderView()
|
||||||
initOtherSessionsView()
|
initOtherSessionsView()
|
||||||
initSecurityRecommendationsView()
|
initSecurityRecommendationsView()
|
||||||
initQrLoginView()
|
initQrLoginView()
|
||||||
|
@ -100,10 +109,7 @@ class VectorSettingsDevicesFragment :
|
||||||
private fun observeViewEvents() {
|
private fun observeViewEvents() {
|
||||||
viewModel.observeViewEvents {
|
viewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is DevicesViewEvent.Loading -> showLoading(it.message)
|
is DevicesViewEvent.RequestReAuth -> askForReAuthentication(it)
|
||||||
is DevicesViewEvent.Failure -> showFailure(it.throwable)
|
|
||||||
is DevicesViewEvent.RequestReAuth -> Unit // TODO. Next PR
|
|
||||||
is DevicesViewEvent.PromptRenameDevice -> Unit // TODO. Next PR
|
|
||||||
is DevicesViewEvent.ShowVerifyDevice -> {
|
is DevicesViewEvent.ShowVerifyDevice -> {
|
||||||
VerificationBottomSheet.withArgs(
|
VerificationBottomSheet.withArgs(
|
||||||
roomId = null,
|
roomId = null,
|
||||||
|
@ -122,6 +128,8 @@ class VectorSettingsDevicesFragment :
|
||||||
is DevicesViewEvent.PromptResetSecrets -> {
|
is DevicesViewEvent.PromptResetSecrets -> {
|
||||||
navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
|
navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
|
||||||
}
|
}
|
||||||
|
is DevicesViewEvent.SignoutError -> showFailure(it.error)
|
||||||
|
is DevicesViewEvent.SignoutSuccess -> Unit // do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,6 +139,29 @@ class VectorSettingsDevicesFragment :
|
||||||
views.waitingView.waitingStatusText.isVisible = true
|
views.waitingView.waitingStatusText.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initOtherSessionsHeaderView() {
|
||||||
|
views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.otherSessionsHeaderMultiSignout -> {
|
||||||
|
confirmMultiSignoutOtherSessions()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun confirmMultiSignoutOtherSessions() {
|
||||||
|
activity?.let {
|
||||||
|
buildConfirmSignoutDialogUseCase.execute(it, this::multiSignoutOtherSessions)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun multiSignoutOtherSessions() {
|
||||||
|
viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
|
||||||
|
}
|
||||||
|
|
||||||
private fun initOtherSessionsView() {
|
private fun initOtherSessionsView() {
|
||||||
views.deviceListOtherSessions.callback = this
|
views.deviceListOtherSessions.callback = this
|
||||||
}
|
}
|
||||||
|
@ -142,7 +173,7 @@ class VectorSettingsDevicesFragment :
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
R.string.device_manager_header_section_security_recommendations_title,
|
R.string.device_manager_header_section_security_recommendations_title,
|
||||||
DeviceManagerFilterType.UNVERIFIED,
|
DeviceManagerFilterType.UNVERIFIED,
|
||||||
excludeCurrentDevice = false
|
excludeCurrentDevice = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,7 +183,7 @@ class VectorSettingsDevicesFragment :
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
R.string.device_manager_header_section_security_recommendations_title,
|
R.string.device_manager_header_section_security_recommendations_title,
|
||||||
DeviceManagerFilterType.INACTIVE,
|
DeviceManagerFilterType.INACTIVE,
|
||||||
excludeCurrentDevice = false
|
excludeCurrentDevice = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -271,6 +302,11 @@ class VectorSettingsDevicesFragment :
|
||||||
hideOtherSessionsView()
|
hideOtherSessionsView()
|
||||||
} else {
|
} else {
|
||||||
views.deviceListHeaderOtherSessions.isVisible = true
|
views.deviceListHeaderOtherSessions.isVisible = true
|
||||||
|
val color = colorProvider.getColorFromAttribute(R.attr.colorError)
|
||||||
|
val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout)
|
||||||
|
val nbDevices = otherDevices.size
|
||||||
|
multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
|
||||||
|
multiSignoutItem.setTextColor(color)
|
||||||
views.deviceListOtherSessions.isVisible = true
|
views.deviceListOtherSessions.isVisible = true
|
||||||
views.deviceListOtherSessions.render(
|
views.deviceListOtherSessions.render(
|
||||||
devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER),
|
devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER),
|
||||||
|
@ -347,4 +383,37 @@ class VectorSettingsDevicesFragment :
|
||||||
excludeCurrentDevice = true
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,10 @@ import android.content.Context
|
||||||
import android.content.res.TypedArray
|
import android.content.res.TypedArray
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import androidx.appcompat.view.menu.MenuBuilder
|
||||||
|
import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.res.use
|
import androidx.core.content.res.use
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
@ -39,6 +43,7 @@ class SessionsListHeaderView @JvmOverloads constructor(
|
||||||
this
|
this
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val menu: Menu = binding.sessionsListHeaderMenu.menu
|
||||||
var onLearnMoreClickListener: (() -> Unit)? = null
|
var onLearnMoreClickListener: (() -> Unit)? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -50,6 +55,7 @@ class SessionsListHeaderView @JvmOverloads constructor(
|
||||||
).use {
|
).use {
|
||||||
setTitle(it)
|
setTitle(it)
|
||||||
setDescription(it)
|
setDescription(it)
|
||||||
|
setMenu(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,4 +96,19 @@ class SessionsListHeaderView @JvmOverloads constructor(
|
||||||
onLearnMoreClickListener?.invoke()
|
onLearnMoreClickListener?.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setMenu(typedArray: TypedArray) {
|
||||||
|
val menuResId = typedArray.getResourceId(R.styleable.SessionsListHeaderView_sessionsListHeaderMenu, -1)
|
||||||
|
if (menuResId == -1) {
|
||||||
|
binding.sessionsListHeaderMenu.isVisible = false
|
||||||
|
} else {
|
||||||
|
binding.sessionsListHeaderMenu.showOverflowMenu()
|
||||||
|
val menuBuilder = binding.sessionsListHeaderMenu.menu as? MenuBuilder
|
||||||
|
menuBuilder?.let { MenuInflater(context).inflate(menuResId, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnMenuItemClickListener(listener: OnMenuItemClickListener) {
|
||||||
|
binding.sessionsListHeaderMenu.setOnMenuItemClickListener(listener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,17 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
|
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
|
||||||
|
|
||||||
sealed class OtherSessionsAction : VectorViewModelAction {
|
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 FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction()
|
||||||
data class EnableSelectMode(val deviceId: String?) : OtherSessionsAction()
|
data class EnableSelectMode(val deviceId: String?) : OtherSessionsAction()
|
||||||
object DisableSelectMode : OtherSessionsAction()
|
object DisableSelectMode : OtherSessionsAction()
|
||||||
data class ToggleSelectionForDevice(val deviceId: String) : OtherSessionsAction()
|
data class ToggleSelectionForDevice(val deviceId: String) : OtherSessionsAction()
|
||||||
object SelectAll : OtherSessionsAction()
|
object SelectAll : OtherSessionsAction()
|
||||||
object DeselectAll : OtherSessionsAction()
|
object DeselectAll : OtherSessionsAction()
|
||||||
|
object MultiSignout : OtherSessionsAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.settings.devices.v2.othersessions
|
package im.vector.app.features.settings.devices.v2.othersessions
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
|
@ -32,6 +33,8 @@ import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
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
|
||||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK
|
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
@ -39,13 +42,16 @@ import im.vector.app.core.platform.VectorMenuProvider
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.databinding.FragmentOtherSessionsBinding
|
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.DeviceFullInfo
|
||||||
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet
|
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet
|
||||||
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
|
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
|
||||||
import im.vector.app.features.settings.devices.v2.list.OtherSessionsView
|
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.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
|
||||||
import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet
|
import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet
|
||||||
|
import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
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 org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -65,6 +71,8 @@ class OtherSessionsFragment :
|
||||||
|
|
||||||
@Inject lateinit var viewNavigator: OtherSessionsViewNavigator
|
@Inject lateinit var viewNavigator: OtherSessionsViewNavigator
|
||||||
|
|
||||||
|
@Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding {
|
||||||
return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false)
|
return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false)
|
||||||
}
|
}
|
||||||
|
@ -77,9 +85,33 @@ class OtherSessionsFragment :
|
||||||
menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled
|
menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled
|
||||||
menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled
|
menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled
|
||||||
menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse()
|
menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse()
|
||||||
|
updateMultiSignoutMenuItem(menu, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateMultiSignoutMenuItem(menu: Menu, viewState: OtherSessionsViewState) {
|
||||||
|
val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout)
|
||||||
|
multiSignoutItem.title = if (viewState.isSelectModeEnabled) {
|
||||||
|
getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase()
|
||||||
|
} else {
|
||||||
|
val nbDevices = viewState.devices()?.size ?: 0
|
||||||
|
stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
|
||||||
|
}
|
||||||
|
multiSignoutItem.isVisible = if (viewState.isSelectModeEnabled) {
|
||||||
|
viewState.devices.invoke()?.any { it.isSelected }.orFalse()
|
||||||
|
} else {
|
||||||
|
viewState.devices.invoke()?.isNotEmpty().orFalse()
|
||||||
|
}
|
||||||
|
val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER
|
||||||
|
multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT)
|
||||||
|
changeTextColorOfDestructiveAction(multiSignoutItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) {
|
||||||
|
val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError)
|
||||||
|
menuItem.setTextColor(titleColor)
|
||||||
|
}
|
||||||
|
|
||||||
override fun handleMenuItemSelected(item: MenuItem): Boolean {
|
override fun handleMenuItemSelected(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.otherSessionsSelect -> {
|
R.id.otherSessionsSelect -> {
|
||||||
|
@ -94,10 +126,25 @@ class OtherSessionsFragment :
|
||||||
viewModel.handle(OtherSessionsAction.DeselectAll)
|
viewModel.handle(OtherSessionsAction.DeselectAll)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.otherSessionsMultiSignout -> {
|
||||||
|
confirmMultiSignout()
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun confirmMultiSignout() {
|
||||||
|
activity?.let {
|
||||||
|
buildConfirmSignoutDialogUseCase.execute(it, this::multiSignout)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun multiSignout() {
|
||||||
|
viewModel.handle(OtherSessionsAction.MultiSignout)
|
||||||
|
}
|
||||||
|
|
||||||
private fun enableSelectMode(isEnabled: Boolean, deviceId: String? = null) {
|
private fun enableSelectMode(isEnabled: Boolean, deviceId: String? = null) {
|
||||||
val action = if (isEnabled) OtherSessionsAction.EnableSelectMode(deviceId) else OtherSessionsAction.DisableSelectMode
|
val action = if (isEnabled) OtherSessionsAction.EnableSelectMode(deviceId) else OtherSessionsAction.DisableSelectMode
|
||||||
viewModel.handle(action)
|
viewModel.handle(action)
|
||||||
|
@ -129,8 +176,9 @@ class OtherSessionsFragment :
|
||||||
private fun observeViewEvents() {
|
private fun observeViewEvents() {
|
||||||
viewModel.observeViewEvents {
|
viewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is OtherSessionsViewEvents.Loading -> showLoading(it.message)
|
is OtherSessionsViewEvents.SignoutError -> showFailure(it.error)
|
||||||
is OtherSessionsViewEvents.Failure -> showFailure(it.throwable)
|
is OtherSessionsViewEvents.RequestReAuth -> askForReAuthentication(it)
|
||||||
|
OtherSessionsViewEvents.SignoutSuccess -> enableSelectMode(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,6 +210,7 @@ class OtherSessionsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
updateLoading(state.isLoading)
|
||||||
if (state.devices is Success) {
|
if (state.devices is Success) {
|
||||||
val devices = state.devices.invoke()
|
val devices = state.devices.invoke()
|
||||||
renderDevices(devices, state.currentFilter)
|
renderDevices(devices, state.currentFilter)
|
||||||
|
@ -169,6 +218,14 @@ class OtherSessionsFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateLoading(isLoading: Boolean) {
|
||||||
|
if (isLoading) {
|
||||||
|
showLoading(null)
|
||||||
|
} else {
|
||||||
|
dismissLoadingDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateToolbar(devices: List<DeviceFullInfo>, isSelectModeEnabled: Boolean) {
|
private fun updateToolbar(devices: List<DeviceFullInfo>, isSelectModeEnabled: Boolean) {
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
val title = if (isSelectModeEnabled) {
|
val title = if (isSelectModeEnabled) {
|
||||||
|
@ -286,4 +343,37 @@ class OtherSessionsFragment :
|
||||||
override fun onViewAllOtherSessionsClicked() {
|
override fun onViewAllOtherSessionsClicked() {
|
||||||
// NOOP. We don't have this button in this screen
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,14 @@
|
||||||
package im.vector.app.features.settings.devices.v2.othersessions
|
package im.vector.app.features.settings.devices.v2.othersessions
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
|
||||||
sealed class OtherSessionsViewEvents : VectorViewEvents {
|
sealed class OtherSessionsViewEvents : VectorViewEvents {
|
||||||
data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents()
|
data class RequestReAuth(
|
||||||
data class Failure(val throwable: Throwable) : OtherSessionsViewEvents()
|
val registrationFlowResponse: RegistrationFlowResponse,
|
||||||
|
val lastErrorCode: String?
|
||||||
|
) : OtherSessionsViewEvents()
|
||||||
|
|
||||||
|
object SignoutSuccess : OtherSessionsViewEvents()
|
||||||
|
data class SignoutError(val error: Throwable) : OtherSessionsViewEvents()
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,16 +24,24 @@ import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
|
import im.vector.app.features.auth.PendingAuthHandler
|
||||||
import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase
|
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.RefreshDevicesUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
|
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.filter.DeviceManagerFilterType
|
||||||
|
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
|
||||||
|
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
class OtherSessionsViewModel @AssistedInject constructor(
|
class OtherSessionsViewModel @AssistedInject constructor(
|
||||||
@Assisted private val initialState: OtherSessionsViewState,
|
@Assisted private val initialState: OtherSessionsViewState,
|
||||||
activeSessionHolder: ActiveSessionHolder,
|
activeSessionHolder: ActiveSessionHolder,
|
||||||
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
|
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
|
||||||
|
private val signoutSessionsUseCase: SignoutSessionsUseCase,
|
||||||
|
private val pendingAuthHandler: PendingAuthHandler,
|
||||||
refreshDevicesUseCase: RefreshDevicesUseCase
|
refreshDevicesUseCase: RefreshDevicesUseCase
|
||||||
) : VectorSessionsListViewModel<OtherSessionsViewState, OtherSessionsAction, OtherSessionsViewEvents>(
|
) : VectorSessionsListViewModel<OtherSessionsViewState, OtherSessionsAction, OtherSessionsViewEvents>(
|
||||||
initialState, activeSessionHolder, refreshDevicesUseCase
|
initialState, activeSessionHolder, refreshDevicesUseCase
|
||||||
|
@ -67,12 +75,16 @@ class OtherSessionsViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
override fun handle(action: OtherSessionsAction) {
|
override fun handle(action: OtherSessionsAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
is OtherSessionsAction.PasswordAuthDone -> handlePasswordAuthDone(action)
|
||||||
|
OtherSessionsAction.ReAuthCancelled -> handleReAuthCancelled()
|
||||||
|
OtherSessionsAction.SsoAuthDone -> handleSsoAuthDone()
|
||||||
is OtherSessionsAction.FilterDevices -> handleFilterDevices(action)
|
is OtherSessionsAction.FilterDevices -> handleFilterDevices(action)
|
||||||
OtherSessionsAction.DisableSelectMode -> handleDisableSelectMode()
|
OtherSessionsAction.DisableSelectMode -> handleDisableSelectMode()
|
||||||
is OtherSessionsAction.EnableSelectMode -> handleEnableSelectMode(action.deviceId)
|
is OtherSessionsAction.EnableSelectMode -> handleEnableSelectMode(action.deviceId)
|
||||||
is OtherSessionsAction.ToggleSelectionForDevice -> handleToggleSelectionForDevice(action.deviceId)
|
is OtherSessionsAction.ToggleSelectionForDevice -> handleToggleSelectionForDevice(action.deviceId)
|
||||||
OtherSessionsAction.DeselectAll -> handleDeselectAll()
|
OtherSessionsAction.DeselectAll -> handleDeselectAll()
|
||||||
OtherSessionsAction.SelectAll -> handleSelectAll()
|
OtherSessionsAction.SelectAll -> handleSelectAll()
|
||||||
|
OtherSessionsAction.MultiSignout -> handleMultiSignout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,4 +154,67 @@ class OtherSessionsViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleMultiSignout() = withState { state ->
|
||||||
|
viewModelScope.launch {
|
||||||
|
setLoading(true)
|
||||||
|
val deviceIds = getDeviceIdsToSignout(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 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, this::onReAuthNeeded)
|
||||||
|
|
||||||
|
private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
|
||||||
|
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)
|
||||||
|
_viewEvents.post(OtherSessionsViewEvents.SignoutError(failure))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSsoAuthDone() {
|
||||||
|
pendingAuthHandler.ssoAuthDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePasswordAuthDone(action: OtherSessionsAction.PasswordAuthDone) {
|
||||||
|
pendingAuthHandler.passwordAuthDone(action.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReAuthCancelled() {
|
||||||
|
pendingAuthHandler.reAuthCancelled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ data class OtherSessionsViewState(
|
||||||
val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS,
|
val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS,
|
||||||
val excludeCurrentDevice: Boolean = false,
|
val excludeCurrentDevice: Boolean = false,
|
||||||
val isSelectModeEnabled: Boolean = false,
|
val isSelectModeEnabled: Boolean = false,
|
||||||
|
val isLoading: Boolean = false,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice)
|
constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice)
|
||||||
|
|
|
@ -29,7 +29,6 @@ import androidx.core.view.isVisible
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.date.VectorDateFormatter
|
import im.vector.app.core.date.VectorDateFormatter
|
||||||
|
@ -45,6 +44,7 @@ import im.vector.app.features.crypto.recover.SetupMode
|
||||||
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
|
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
|
||||||
import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet
|
import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet
|
||||||
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
|
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
|
||||||
|
import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase
|
||||||
import im.vector.app.features.workers.signout.SignOutUiWorker
|
import im.vector.app.features.workers.signout.SignOutUiWorker
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
@ -69,6 +69,8 @@ class SessionOverviewFragment :
|
||||||
|
|
||||||
@Inject lateinit var stringProvider: StringProvider
|
@Inject lateinit var stringProvider: StringProvider
|
||||||
|
|
||||||
|
@Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase
|
||||||
|
|
||||||
private val viewModel: SessionOverviewViewModel by fragmentViewModel()
|
private val viewModel: SessionOverviewViewModel by fragmentViewModel()
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionOverviewBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionOverviewBinding {
|
||||||
|
@ -134,13 +136,7 @@ class SessionOverviewFragment :
|
||||||
|
|
||||||
private fun confirmSignoutOtherSession() {
|
private fun confirmSignoutOtherSession() {
|
||||||
activity?.let {
|
activity?.let {
|
||||||
MaterialAlertDialogBuilder(it)
|
buildConfirmSignoutDialogUseCase.execute(it, this::signoutSession)
|
||||||
.setTitle(R.string.action_sign_out)
|
|
||||||
.setMessage(R.string.action_sign_out_confirmation_simple)
|
|
||||||
.setPositiveButton(R.string.action_sign_out) { _, _ ->
|
|
||||||
signoutSession()
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.action_cancel, null)
|
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,42 +21,33 @@ import com.airbnb.mvrx.Success
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.R
|
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
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.auth.PendingAuthHandler
|
||||||
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
|
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.VectorSessionsListViewModel
|
||||||
import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
|
import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
|
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
|
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.SignoutSessionsReAuthNeeded
|
||||||
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase
|
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.CheckIfCurrentSessionCanBeVerifiedUseCase
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
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.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
|
|
||||||
class SessionOverviewViewModel @AssistedInject constructor(
|
class SessionOverviewViewModel @AssistedInject constructor(
|
||||||
@Assisted val initialState: SessionOverviewViewState,
|
@Assisted val initialState: SessionOverviewViewState,
|
||||||
private val stringProvider: StringProvider,
|
|
||||||
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
|
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
|
||||||
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
|
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
|
||||||
private val signoutSessionUseCase: SignoutSessionUseCase,
|
private val signoutSessionsUseCase: SignoutSessionsUseCase,
|
||||||
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
|
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
|
||||||
private val pendingAuthHandler: PendingAuthHandler,
|
private val pendingAuthHandler: PendingAuthHandler,
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
@ -154,30 +145,21 @@ class SessionOverviewViewModel @AssistedInject constructor(
|
||||||
private fun handleSignoutOtherSession(deviceId: String) {
|
private fun handleSignoutOtherSession(deviceId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
val signoutResult = signout(deviceId)
|
val result = signout(deviceId)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
if (signoutResult.isSuccess) {
|
val error = result.exceptionOrNull()
|
||||||
|
if (error == null) {
|
||||||
onSignoutSuccess()
|
onSignoutSuccess()
|
||||||
} else {
|
} else {
|
||||||
when (val failure = signoutResult.exceptionOrNull()) {
|
onSignoutFailure(error)
|
||||||
null -> onSignoutSuccess()
|
|
||||||
else -> onSignoutFailure(failure)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun signout(deviceId: String) = signoutSessionUseCase.execute(deviceId, object : UserInteractiveAuthInterceptor {
|
private suspend fun signout(deviceId: String) = signoutSessionsUseCase.execute(listOf(deviceId), this::onReAuthNeeded)
|
||||||
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) {
|
private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
|
||||||
Timber.d("onReAuthNeeded")
|
Timber.d("onReAuthNeeded")
|
||||||
pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
|
pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
|
||||||
pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation
|
pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation
|
||||||
|
@ -196,12 +178,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
private fun onSignoutFailure(failure: Throwable) {
|
private fun onSignoutFailure(failure: Throwable) {
|
||||||
Timber.e("signout failure", failure)
|
Timber.e("signout failure", failure)
|
||||||
val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
|
_viewEvents.post(SessionOverviewViewEvent.SignoutError(failure))
|
||||||
stringProvider.getString(R.string.authentication_error)
|
|
||||||
} else {
|
|
||||||
stringProvider.getString(R.string.matrix_error)
|
|
||||||
}
|
|
||||||
_viewEvents.post(SessionOverviewViewEvent.SignoutError(Exception(failureMessage)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSsoAuthDone() {
|
private fun handleSsoAuthDone() {
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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 android.content.Context
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import im.vector.app.R
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class BuildConfirmSignoutDialogUseCase @Inject constructor() {
|
||||||
|
|
||||||
|
fun execute(context: Context, onConfirm: () -> Unit) =
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(R.string.action_sign_out)
|
||||||
|
.setMessage(R.string.action_sign_out_confirmation_simple)
|
||||||
|
.setPositiveButton(R.string.action_sign_out) { _, _ ->
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.action_cancel, null)
|
||||||
|
.create()
|
||||||
|
}
|
|
@ -37,17 +37,16 @@ class InterceptSignoutFlowResponseUseCase @Inject constructor(
|
||||||
flowResponse: RegistrationFlowResponse,
|
flowResponse: RegistrationFlowResponse,
|
||||||
errCode: String?,
|
errCode: String?,
|
||||||
promise: Continuation<UIABaseAuth>
|
promise: Continuation<UIABaseAuth>
|
||||||
): SignoutSessionResult {
|
): SignoutSessionsReAuthNeeded? {
|
||||||
return if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) {
|
return if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) {
|
||||||
UserPasswordAuth(
|
UserPasswordAuth(
|
||||||
session = null,
|
session = null,
|
||||||
user = activeSessionHolder.getActiveSession().myUserId,
|
user = activeSessionHolder.getActiveSession().myUserId,
|
||||||
password = reAuthHelper.data
|
password = reAuthHelper.data
|
||||||
).let { promise.resume(it) }
|
).let { promise.resume(it) }
|
||||||
|
null
|
||||||
SignoutSessionResult.Completed
|
|
||||||
} else {
|
} else {
|
||||||
SignoutSessionResult.ReAuthNeeded(
|
SignoutSessionsReAuthNeeded(
|
||||||
pendingAuth = DefaultBaseAuth(session = flowResponse.session),
|
pendingAuth = DefaultBaseAuth(session = flowResponse.session),
|
||||||
uiaContinuation = promise,
|
uiaContinuation = promise,
|
||||||
flowResponse = flowResponse,
|
flowResponse = flowResponse,
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
|
|
||||||
class SignoutSessionUseCase @Inject constructor(
|
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend fun execute(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result<Unit> {
|
|
||||||
return deleteDevice(deviceId, userInteractiveAuthInterceptor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching {
|
|
||||||
awaitCallback { matrixCallback ->
|
|
||||||
activeSessionHolder.getActiveSession()
|
|
||||||
.cryptoService()
|
|
||||||
.deleteDevice(deviceId, userInteractiveAuthInterceptor, matrixCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,13 +20,9 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
|
|
||||||
sealed class SignoutSessionResult {
|
data class SignoutSessionsReAuthNeeded(
|
||||||
data class ReAuthNeeded(
|
val pendingAuth: UIABaseAuth,
|
||||||
val pendingAuth: UIABaseAuth,
|
val uiaContinuation: Continuation<UIABaseAuth>,
|
||||||
val uiaContinuation: Continuation<UIABaseAuth>,
|
val flowResponse: RegistrationFlowResponse,
|
||||||
val flowResponse: RegistrationFlowResponse,
|
val errCode: String?
|
||||||
val errCode: String?
|
)
|
||||||
) : SignoutSessionResult()
|
|
||||||
|
|
||||||
object Completed : SignoutSessionResult()
|
|
||||||
}
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 androidx.annotation.Size
|
||||||
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
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.util.awaitCallback
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
|
||||||
|
class SignoutSessionsUseCase @Inject constructor(
|
||||||
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun execute(
|
||||||
|
@Size(min = 1) deviceIds: List<String>,
|
||||||
|
onReAuthNeeded: (SignoutSessionsReAuthNeeded) -> Unit,
|
||||||
|
): Result<Unit> = runCatching {
|
||||||
|
Timber.d("start execute with ${deviceIds.size} deviceIds")
|
||||||
|
|
||||||
|
val authInterceptor = object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)
|
||||||
|
result?.let(onReAuthNeeded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDevices(deviceIds, authInterceptor)
|
||||||
|
Timber.d("end execute")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun deleteDevices(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) =
|
||||||
|
awaitCallback { matrixCallback ->
|
||||||
|
activeSessionHolder.getActiveSession()
|
||||||
|
.cryptoService()
|
||||||
|
.deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback)
|
||||||
|
}
|
||||||
|
}
|
|
@ -98,6 +98,7 @@
|
||||||
app:layout_constraintTop_toBottomOf="@id/deviceListDividerCurrentSession"
|
app:layout_constraintTop_toBottomOf="@id/deviceListDividerCurrentSession"
|
||||||
app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
|
app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
|
||||||
app:sessionsListHeaderHasLearnMoreLink="false"
|
app:sessionsListHeaderHasLearnMoreLink="false"
|
||||||
|
app:sessionsListHeaderMenu="@menu/menu_other_sessions_header"
|
||||||
app:sessionsListHeaderTitle="@string/device_manager_sessions_other_title" />
|
app:sessionsListHeaderTitle="@string/device_manager_sessions_other_title" />
|
||||||
|
|
||||||
<im.vector.app.features.settings.devices.v2.list.OtherSessionsView
|
<im.vector.app.features.settings.devices.v2.list.OtherSessionsView
|
||||||
|
@ -117,8 +118,8 @@
|
||||||
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/deviceListOtherSessions"
|
app:layout_constraintTop_toBottomOf="@id/deviceListOtherSessions"
|
||||||
app:sessionsListHeaderHasLearnMoreLink="false"
|
|
||||||
app:sessionsListHeaderDescription="@string/device_manager_sessions_sign_in_with_qr_code_description"
|
app:sessionsListHeaderDescription="@string/device_manager_sessions_sign_in_with_qr_code_description"
|
||||||
|
app:sessionsListHeaderHasLearnMoreLink="false"
|
||||||
app:sessionsListHeaderTitle="@string/device_manager_sessions_sign_in_with_qr_code_title"
|
app:sessionsListHeaderTitle="@string/device_manager_sessions_sign_in_with_qr_code_title"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
|
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_marginTop="20dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toStartOf="@id/sessionsListHeaderMenu"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="Other sessions" />
|
tools:text="Other sessions" />
|
||||||
|
@ -29,4 +29,13 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title"
|
app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title"
|
||||||
tools:text="For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Learn More." />
|
tools:text="For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Learn More." />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.ActionMenuView
|
||||||
|
android:id="@+id/sessionsListHeaderMenu"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/sessions_list_header_title"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/sessions_list_header_title" />
|
||||||
</merge>
|
</merge>
|
||||||
|
|
|
@ -9,6 +9,11 @@
|
||||||
android:title="@string/device_manager_other_sessions_select"
|
android:title="@string/device_manager_other_sessions_select"
|
||||||
app:showAsAction="withText|never" />
|
app:showAsAction="withText|never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/otherSessionsMultiSignout"
|
||||||
|
android:title="@plurals/device_manager_other_sessions_multi_signout_all"
|
||||||
|
app:showAsAction="withText|never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/otherSessionsSelectAll"
|
android:id="@+id/otherSessionsSelectAll"
|
||||||
android:title="@string/action_select_all"
|
android:title="@string/action_select_all"
|
||||||
|
|
12
vector/src/main/res/menu/menu_other_sessions_header.xml
Normal file
12
vector/src/main/res/menu/menu_other_sessions_header.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:ignore="AlwaysShowAction">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/otherSessionsHeaderMultiSignout"
|
||||||
|
android:title="@plurals/device_manager_other_sessions_multi_signout_all"
|
||||||
|
app:showAsAction="withText|never" />
|
||||||
|
|
||||||
|
</menu>
|
|
@ -22,30 +22,41 @@ import com.airbnb.mvrx.test.MavericksTestRule
|
||||||
import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
|
import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
|
||||||
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
|
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
|
||||||
import im.vector.app.features.settings.devices.v2.list.DeviceType
|
import im.vector.app.features.settings.devices.v2.list.DeviceType
|
||||||
|
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo
|
import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo
|
||||||
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
|
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
|
||||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||||
|
import im.vector.app.test.fakes.FakePendingAuthHandler
|
||||||
|
import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
|
||||||
import im.vector.app.test.fakes.FakeVerificationService
|
import im.vector.app.test.fakes.FakeVerificationService
|
||||||
import im.vector.app.test.test
|
import im.vector.app.test.test
|
||||||
import im.vector.app.test.testDispatcher
|
import im.vector.app.test.testDispatcher
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.justRun
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.runs
|
|
||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import io.mockk.verifyAll
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||||
|
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
||||||
|
|
||||||
|
private const val A_CURRENT_DEVICE_ID = "current-device-id"
|
||||||
|
private const val A_DEVICE_ID_1 = "device-id-1"
|
||||||
|
private const val A_DEVICE_ID_2 = "device-id-2"
|
||||||
|
private const val A_PASSWORD = "password"
|
||||||
|
|
||||||
class DevicesViewModelTest {
|
class DevicesViewModelTest {
|
||||||
|
|
||||||
|
@ -55,19 +66,25 @@ class DevicesViewModelTest {
|
||||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||||
private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
|
private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
|
||||||
private val getDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
|
private val getDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
|
||||||
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxUnitFun = true)
|
private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>(relaxed = true)
|
||||||
private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>()
|
|
||||||
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
|
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
|
||||||
|
private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
|
||||||
|
private val fakeInterceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
|
||||||
|
private val fakePendingAuthHandler = FakePendingAuthHandler()
|
||||||
|
private val fakeRefreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxUnitFun = true)
|
||||||
|
|
||||||
private fun createViewModel(): DevicesViewModel {
|
private fun createViewModel(): DevicesViewModel {
|
||||||
return DevicesViewModel(
|
return DevicesViewModel(
|
||||||
DevicesViewState(),
|
initialState = DevicesViewState(),
|
||||||
fakeActiveSessionHolder.instance,
|
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||||
getCurrentSessionCrossSigningInfoUseCase,
|
getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
|
||||||
getDeviceFullInfoListUseCase,
|
getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase,
|
||||||
refreshDevicesOnCryptoDevicesChangeUseCase,
|
refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase,
|
||||||
checkIfCurrentSessionCanBeVerifiedUseCase,
|
checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
|
||||||
refreshDevicesUseCase,
|
signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
|
||||||
|
interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase,
|
||||||
|
pendingAuthHandler = fakePendingAuthHandler.instance,
|
||||||
|
refreshDevicesUseCase = fakeRefreshDevicesUseCase,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +93,20 @@ class DevicesViewModelTest {
|
||||||
// Needed for internal usage of Flow<T>.throttleFirst() inside the ViewModel
|
// Needed for internal usage of Flow<T>.throttleFirst() inside the ViewModel
|
||||||
mockkStatic(SystemClock::class)
|
mockkStatic(SystemClock::class)
|
||||||
every { SystemClock.elapsedRealtime() } returns 1234
|
every { SystemClock.elapsedRealtime() } returns 1234
|
||||||
|
|
||||||
|
givenVerificationService()
|
||||||
|
givenCurrentSessionCrossSigningInfo()
|
||||||
|
givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenVerificationService(): FakeVerificationService {
|
||||||
|
val fakeVerificationService = fakeActiveSessionHolder
|
||||||
|
.fakeSession
|
||||||
|
.fakeCryptoService
|
||||||
|
.fakeVerificationService
|
||||||
|
fakeVerificationService.givenAddListenerSucceeds()
|
||||||
|
fakeVerificationService.givenRemoveListenerSucceeds()
|
||||||
|
return fakeVerificationService
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -87,9 +118,6 @@ class DevicesViewModelTest {
|
||||||
fun `given the viewModel when initializing it then verification listener is added`() {
|
fun `given the viewModel when initializing it then verification listener is added`() {
|
||||||
// Given
|
// Given
|
||||||
val fakeVerificationService = givenVerificationService()
|
val fakeVerificationService = givenVerificationService()
|
||||||
givenCurrentSessionCrossSigningInfo()
|
|
||||||
givenDeviceFullInfoList()
|
|
||||||
givenRefreshDevicesOnCryptoDevicesChange()
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -104,9 +132,6 @@ class DevicesViewModelTest {
|
||||||
fun `given the viewModel when clearing it then verification listener is removed`() {
|
fun `given the viewModel when clearing it then verification listener is removed`() {
|
||||||
// Given
|
// Given
|
||||||
val fakeVerificationService = givenVerificationService()
|
val fakeVerificationService = givenVerificationService()
|
||||||
givenCurrentSessionCrossSigningInfo()
|
|
||||||
givenDeviceFullInfoList()
|
|
||||||
givenRefreshDevicesOnCryptoDevicesChange()
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -121,10 +146,7 @@ class DevicesViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given the viewModel when initializing it then view state is updated with current session cross signing info`() {
|
fun `given the viewModel when initializing it then view state is updated with current session cross signing info`() {
|
||||||
// Given
|
// Given
|
||||||
givenVerificationService()
|
|
||||||
val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
|
val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
|
||||||
givenDeviceFullInfoList()
|
|
||||||
givenRefreshDevicesOnCryptoDevicesChange()
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val viewModelTest = createViewModel().test()
|
val viewModelTest = createViewModel().test()
|
||||||
|
@ -137,10 +159,7 @@ class DevicesViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given the viewModel when initializing it then view state is updated with current device full info list`() {
|
fun `given the viewModel when initializing it then view state is updated with current device full info list`() {
|
||||||
// Given
|
// Given
|
||||||
givenVerificationService()
|
val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2)
|
||||||
givenCurrentSessionCrossSigningInfo()
|
|
||||||
val deviceFullInfoList = givenDeviceFullInfoList()
|
|
||||||
givenRefreshDevicesOnCryptoDevicesChange()
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val viewModelTest = createViewModel().test()
|
val viewModelTest = createViewModel().test()
|
||||||
|
@ -156,10 +175,6 @@ class DevicesViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given the viewModel when initializing it then devices are refreshed on crypto devices change`() {
|
fun `given the viewModel when initializing it then devices are refreshed on crypto devices change`() {
|
||||||
// Given
|
// Given
|
||||||
givenVerificationService()
|
|
||||||
givenCurrentSessionCrossSigningInfo()
|
|
||||||
givenDeviceFullInfoList()
|
|
||||||
givenRefreshDevicesOnCryptoDevicesChange()
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
createViewModel()
|
createViewModel()
|
||||||
|
@ -171,10 +186,6 @@ class DevicesViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given current session can be verified when handling verify current session action then self verification event is posted`() {
|
fun `given current session can be verified when handling verify current session action then self verification event is posted`() {
|
||||||
// Given
|
// Given
|
||||||
givenVerificationService()
|
|
||||||
givenCurrentSessionCrossSigningInfo()
|
|
||||||
givenDeviceFullInfoList()
|
|
||||||
givenRefreshDevicesOnCryptoDevicesChange()
|
|
||||||
val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession
|
val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession
|
||||||
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true
|
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true
|
||||||
|
|
||||||
|
@ -195,10 +206,6 @@ class DevicesViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given current session cannot be verified when handling verify current session action then reset secrets event is posted`() {
|
fun `given current session cannot be verified when handling verify current session action then reset secrets event is posted`() {
|
||||||
// Given
|
// Given
|
||||||
givenVerificationService()
|
|
||||||
givenCurrentSessionCrossSigningInfo()
|
|
||||||
givenDeviceFullInfoList()
|
|
||||||
givenRefreshDevicesOnCryptoDevicesChange()
|
|
||||||
val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession
|
val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession
|
||||||
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false
|
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false
|
||||||
|
|
||||||
|
@ -216,18 +223,129 @@ class DevicesViewModelTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenVerificationService(): FakeVerificationService {
|
@Test
|
||||||
val fakeVerificationService = fakeActiveSessionHolder
|
fun `given no reAuth is needed when handling multiSignout other sessions action then signout process is performed`() {
|
||||||
.fakeSession
|
// Given
|
||||||
.fakeCryptoService
|
val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_CURRENT_DEVICE_ID)
|
||||||
.fakeVerificationService
|
// signout all devices except the current device
|
||||||
fakeVerificationService.givenAddListenerSucceeds()
|
fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1))
|
||||||
fakeVerificationService.givenRemoveListenerSucceeds()
|
|
||||||
return fakeVerificationService
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertStatesChanges(
|
||||||
|
expectedViewState,
|
||||||
|
{ copy(isLoading = true) },
|
||||||
|
{ copy(isLoading = false) }
|
||||||
|
)
|
||||||
|
.assertEvent { it is DevicesViewEvent.SignoutSuccess }
|
||||||
|
.finish()
|
||||||
|
verify {
|
||||||
|
fakeRefreshDevicesUseCase.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() {
|
||||||
|
// Given
|
||||||
|
val error = Exception()
|
||||||
|
fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error)
|
||||||
|
val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertStatesChanges(
|
||||||
|
expectedViewState,
|
||||||
|
{ copy(isLoading = true) },
|
||||||
|
{ copy(isLoading = false) }
|
||||||
|
)
|
||||||
|
.assertEvent { it is DevicesViewEvent.SignoutError && it.error == error }
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() {
|
||||||
|
// Given
|
||||||
|
val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2))
|
||||||
|
val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
|
||||||
|
val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertEvent { it == expectedReAuthEvent }
|
||||||
|
.finish()
|
||||||
|
fakePendingAuthHandler.instance.pendingAuth shouldBeEqualTo expectedPendingAuth
|
||||||
|
fakePendingAuthHandler.instance.uiaContinuation shouldBeEqualTo reAuthNeeded.uiaContinuation
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given SSO auth has been done when handling ssoAuthDone action then corresponding method of pending auth handler is called`() {
|
||||||
|
// Given
|
||||||
|
justRun { fakePendingAuthHandler.instance.ssoAuthDone() }
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(DevicesAction.SsoAuthDone)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest.finish()
|
||||||
|
verifyAll {
|
||||||
|
fakePendingAuthHandler.instance.ssoAuthDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given password auth has been done when handling passwordAuthDone action then corresponding method of pending auth handler is called`() {
|
||||||
|
// Given
|
||||||
|
justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) }
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(DevicesAction.PasswordAuthDone(A_PASSWORD))
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest.finish()
|
||||||
|
verifyAll {
|
||||||
|
fakePendingAuthHandler.instance.passwordAuthDone(A_PASSWORD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given reAuth has been cancelled when handling reAuthCancelled action then corresponding method of pending auth handler is called`() {
|
||||||
|
// Given
|
||||||
|
justRun { fakePendingAuthHandler.instance.reAuthCancelled() }
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(DevicesAction.ReAuthCancelled)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest.finish()
|
||||||
|
verifyAll {
|
||||||
|
fakePendingAuthHandler.instance.reAuthCancelled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
|
private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
|
||||||
val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
|
val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
|
||||||
|
every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID
|
||||||
every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo)
|
every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo)
|
||||||
return currentSessionCrossSigningInfo
|
return currentSessionCrossSigningInfo
|
||||||
}
|
}
|
||||||
|
@ -235,14 +353,19 @@ class DevicesViewModelTest {
|
||||||
/**
|
/**
|
||||||
* Generate mocked deviceFullInfo list with 1 unverified and inactive + 1 verified and active.
|
* Generate mocked deviceFullInfo list with 1 unverified and inactive + 1 verified and active.
|
||||||
*/
|
*/
|
||||||
private fun givenDeviceFullInfoList(): List<DeviceFullInfo> {
|
private fun givenDeviceFullInfoList(deviceId1: String, deviceId2: String): List<DeviceFullInfo> {
|
||||||
val verifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
|
val verifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
|
||||||
every { verifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
|
every { verifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
|
||||||
val unverifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
|
val unverifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
|
||||||
every { unverifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
|
every { unverifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
|
||||||
|
|
||||||
|
val deviceInfo1 = mockk<DeviceInfo>()
|
||||||
|
every { deviceInfo1.deviceId } returns deviceId1
|
||||||
|
val deviceInfo2 = mockk<DeviceInfo>()
|
||||||
|
every { deviceInfo2.deviceId } returns deviceId2
|
||||||
|
|
||||||
val deviceFullInfo1 = DeviceFullInfo(
|
val deviceFullInfo1 = DeviceFullInfo(
|
||||||
deviceInfo = mockk(),
|
deviceInfo = deviceInfo1,
|
||||||
cryptoDeviceInfo = verifiedCryptoDeviceInfo,
|
cryptoDeviceInfo = verifiedCryptoDeviceInfo,
|
||||||
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
|
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
|
||||||
isInactive = false,
|
isInactive = false,
|
||||||
|
@ -251,7 +374,7 @@ class DevicesViewModelTest {
|
||||||
matrixClientInfo = MatrixClientInfoContent(),
|
matrixClientInfo = MatrixClientInfoContent(),
|
||||||
)
|
)
|
||||||
val deviceFullInfo2 = DeviceFullInfo(
|
val deviceFullInfo2 = DeviceFullInfo(
|
||||||
deviceInfo = mockk(),
|
deviceInfo = deviceInfo2,
|
||||||
cryptoDeviceInfo = unverifiedCryptoDeviceInfo,
|
cryptoDeviceInfo = unverifiedCryptoDeviceInfo,
|
||||||
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
|
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
|
||||||
isInactive = true,
|
isInactive = true,
|
||||||
|
@ -265,7 +388,15 @@ class DevicesViewModelTest {
|
||||||
return deviceFullInfoList
|
return deviceFullInfoList
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenRefreshDevicesOnCryptoDevicesChange() {
|
private fun givenInitialViewState(deviceId1: String, deviceId2: String): DevicesViewState {
|
||||||
coEvery { refreshDevicesOnCryptoDevicesChangeUseCase.execute() } just runs
|
val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
|
||||||
|
val deviceFullInfoList = givenDeviceFullInfoList(deviceId1, deviceId2)
|
||||||
|
return DevicesViewState(
|
||||||
|
currentSessionCrossSigningInfo = currentSessionCrossSigningInfo,
|
||||||
|
devices = Success(deviceFullInfoList),
|
||||||
|
unverifiedSessionsCount = 1,
|
||||||
|
inactiveSessionsCount = 1,
|
||||||
|
isLoading = false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,23 +24,31 @@ 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.RefreshDevicesUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
|
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
|
||||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||||
|
import im.vector.app.test.fakes.FakePendingAuthHandler
|
||||||
|
import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
|
||||||
import im.vector.app.test.fakes.FakeVerificationService
|
import im.vector.app.test.fakes.FakeVerificationService
|
||||||
import im.vector.app.test.fixtures.aDeviceFullInfo
|
import im.vector.app.test.fixtures.aDeviceFullInfo
|
||||||
import im.vector.app.test.test
|
import im.vector.app.test.test
|
||||||
import im.vector.app.test.testDispatcher
|
import im.vector.app.test.testDispatcher
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.justRun
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
|
import io.mockk.verify
|
||||||
import io.mockk.verifyAll
|
import io.mockk.verifyAll
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
||||||
|
|
||||||
private const val A_TITLE_RES_ID = 1
|
private const val A_TITLE_RES_ID = 1
|
||||||
private const val A_DEVICE_ID = "device-id"
|
private const val A_DEVICE_ID_1 = "device-id-1"
|
||||||
|
private const val A_DEVICE_ID_2 = "device-id-2"
|
||||||
|
private const val A_PASSWORD = "password"
|
||||||
|
|
||||||
class OtherSessionsViewModelTest {
|
class OtherSessionsViewModelTest {
|
||||||
|
|
||||||
|
@ -55,14 +63,19 @@ class OtherSessionsViewModelTest {
|
||||||
|
|
||||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||||
private val fakeGetDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
|
private val fakeGetDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
|
||||||
private val fakeRefreshDevicesUseCaseUseCase = mockk<RefreshDevicesUseCase>()
|
private val fakeRefreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true)
|
||||||
|
private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
|
||||||
|
private val fakePendingAuthHandler = FakePendingAuthHandler()
|
||||||
|
|
||||||
private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = OtherSessionsViewModel(
|
private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) =
|
||||||
initialState = OtherSessionsViewState(args),
|
OtherSessionsViewModel(
|
||||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
initialState = viewState,
|
||||||
getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase,
|
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||||
refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase,
|
getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase,
|
||||||
)
|
signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
|
||||||
|
pendingAuthHandler = fakePendingAuthHandler.instance,
|
||||||
|
refreshDevicesUseCase = fakeRefreshDevicesUseCase,
|
||||||
|
)
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
@ -88,6 +101,39 @@ class OtherSessionsViewModelTest {
|
||||||
unmockkAll()
|
unmockkAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given the viewModel when initializing it then verification listener is added`() {
|
||||||
|
// Given
|
||||||
|
val fakeVerificationService = givenVerificationService()
|
||||||
|
val devices = mockk<List<DeviceFullInfo>>()
|
||||||
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verify {
|
||||||
|
fakeVerificationService.addListener(viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given the viewModel when clearing it then verification listener is removed`() {
|
||||||
|
// Given
|
||||||
|
val fakeVerificationService = givenVerificationService()
|
||||||
|
val devices = mockk<List<DeviceFullInfo>>()
|
||||||
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.onCleared()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verify {
|
||||||
|
fakeVerificationService.removeListener(viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given the viewModel has been initialized then viewState is updated with devices list`() {
|
fun `given the viewModel has been initialized then viewState is updated with devices list`() {
|
||||||
// Given
|
// Given
|
||||||
|
@ -143,7 +189,7 @@ class OtherSessionsViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given enable select mode action when handling the action then viewState is updated with correct info`() {
|
fun `given enable select mode action when handling the action then viewState is updated with correct info`() {
|
||||||
// Given
|
// Given
|
||||||
val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID, isSelected = false)
|
val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
|
||||||
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo)
|
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo)
|
||||||
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
val expectedState = OtherSessionsViewState(
|
val expectedState = OtherSessionsViewState(
|
||||||
|
@ -156,7 +202,7 @@ class OtherSessionsViewModelTest {
|
||||||
// When
|
// When
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
val viewModelTest = viewModel.test()
|
val viewModelTest = viewModel.test()
|
||||||
viewModel.handle(OtherSessionsAction.EnableSelectMode(A_DEVICE_ID))
|
viewModel.handle(OtherSessionsAction.EnableSelectMode(A_DEVICE_ID_1))
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
viewModelTest
|
viewModelTest
|
||||||
|
@ -167,8 +213,8 @@ class OtherSessionsViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given disable select mode action when handling the action then viewState is updated with correct info`() {
|
fun `given disable select mode action when handling the action then viewState is updated with correct info`() {
|
||||||
// Given
|
// Given
|
||||||
val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true)
|
val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = true)
|
||||||
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true)
|
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = true)
|
||||||
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
|
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
|
||||||
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
val expectedState = OtherSessionsViewState(
|
val expectedState = OtherSessionsViewState(
|
||||||
|
@ -192,7 +238,7 @@ class OtherSessionsViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given toggle selection for device action when handling the action then viewState is updated with correct info`() {
|
fun `given toggle selection for device action when handling the action then viewState is updated with correct info`() {
|
||||||
// Given
|
// Given
|
||||||
val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID, isSelected = false)
|
val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
|
||||||
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo)
|
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo)
|
||||||
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
val expectedState = OtherSessionsViewState(
|
val expectedState = OtherSessionsViewState(
|
||||||
|
@ -205,7 +251,7 @@ class OtherSessionsViewModelTest {
|
||||||
// When
|
// When
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
val viewModelTest = viewModel.test()
|
val viewModelTest = viewModel.test()
|
||||||
viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(A_DEVICE_ID))
|
viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(A_DEVICE_ID_1))
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
viewModelTest
|
viewModelTest
|
||||||
|
@ -216,8 +262,8 @@ class OtherSessionsViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given select all action when handling the action then viewState is updated with correct info`() {
|
fun `given select all action when handling the action then viewState is updated with correct info`() {
|
||||||
// Given
|
// Given
|
||||||
val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = false)
|
val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
|
||||||
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true)
|
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
|
||||||
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
|
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
|
||||||
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
val expectedState = OtherSessionsViewState(
|
val expectedState = OtherSessionsViewState(
|
||||||
|
@ -241,8 +287,8 @@ class OtherSessionsViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given deselect all action when handling the action then viewState is updated with correct info`() {
|
fun `given deselect all action when handling the action then viewState is updated with correct info`() {
|
||||||
// Given
|
// Given
|
||||||
val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = false)
|
val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
|
||||||
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true)
|
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
|
||||||
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
|
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
|
||||||
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
val expectedState = OtherSessionsViewState(
|
val expectedState = OtherSessionsViewState(
|
||||||
|
@ -263,6 +309,190 @@ class OtherSessionsViewModelTest {
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no reAuth is needed and in selectMode when handling multiSignout action then signout process is performed`() {
|
||||||
|
// Given
|
||||||
|
val isSelectModeEnabled = true
|
||||||
|
val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
|
||||||
|
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
|
||||||
|
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
|
||||||
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
|
// signout only selected devices
|
||||||
|
fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2))
|
||||||
|
val expectedViewState = OtherSessionsViewState(
|
||||||
|
devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)),
|
||||||
|
currentFilter = defaultArgs.defaultFilter,
|
||||||
|
excludeCurrentDevice = defaultArgs.excludeCurrentDevice,
|
||||||
|
isSelectModeEnabled = isSelectModeEnabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel(OtherSessionsViewState(defaultArgs).copy(isSelectModeEnabled = isSelectModeEnabled))
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(OtherSessionsAction.MultiSignout)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertStatesChanges(
|
||||||
|
expectedViewState,
|
||||||
|
{ copy(isLoading = true) },
|
||||||
|
{ copy(isLoading = false) }
|
||||||
|
)
|
||||||
|
.assertEvent { it is OtherSessionsViewEvents.SignoutSuccess }
|
||||||
|
.finish()
|
||||||
|
verify {
|
||||||
|
fakeRefreshDevicesUseCase.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no reAuth is needed and NOT in selectMode when handling multiSignout action then signout process is performed`() {
|
||||||
|
// Given
|
||||||
|
val isSelectModeEnabled = false
|
||||||
|
val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
|
||||||
|
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
|
||||||
|
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
|
||||||
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
|
// signout all devices
|
||||||
|
fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2))
|
||||||
|
val expectedViewState = OtherSessionsViewState(
|
||||||
|
devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)),
|
||||||
|
currentFilter = defaultArgs.defaultFilter,
|
||||||
|
excludeCurrentDevice = defaultArgs.excludeCurrentDevice,
|
||||||
|
isSelectModeEnabled = isSelectModeEnabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel(OtherSessionsViewState(defaultArgs).copy(isSelectModeEnabled = isSelectModeEnabled))
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(OtherSessionsAction.MultiSignout)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertStatesChanges(
|
||||||
|
expectedViewState,
|
||||||
|
{ copy(isLoading = true) },
|
||||||
|
{ copy(isLoading = false) }
|
||||||
|
)
|
||||||
|
.assertEvent { it is OtherSessionsViewEvents.SignoutSuccess }
|
||||||
|
.finish()
|
||||||
|
verify {
|
||||||
|
fakeRefreshDevicesUseCase.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() {
|
||||||
|
// Given
|
||||||
|
val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
|
||||||
|
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
|
||||||
|
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
|
||||||
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
|
val error = Exception()
|
||||||
|
fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error)
|
||||||
|
val expectedViewState = OtherSessionsViewState(
|
||||||
|
devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)),
|
||||||
|
currentFilter = defaultArgs.defaultFilter,
|
||||||
|
excludeCurrentDevice = defaultArgs.excludeCurrentDevice,
|
||||||
|
)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(OtherSessionsAction.MultiSignout)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertStatesChanges(
|
||||||
|
expectedViewState,
|
||||||
|
{ copy(isLoading = true) },
|
||||||
|
{ copy(isLoading = false) }
|
||||||
|
)
|
||||||
|
.assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error == error }
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() {
|
||||||
|
// Given
|
||||||
|
val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false)
|
||||||
|
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
|
||||||
|
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
|
||||||
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
|
val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2))
|
||||||
|
val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
|
||||||
|
val expectedReAuthEvent = OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(OtherSessionsAction.MultiSignout)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertEvent { it == expectedReAuthEvent }
|
||||||
|
.finish()
|
||||||
|
fakePendingAuthHandler.instance.pendingAuth shouldBeEqualTo expectedPendingAuth
|
||||||
|
fakePendingAuthHandler.instance.uiaContinuation shouldBeEqualTo reAuthNeeded.uiaContinuation
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given SSO auth has been done when handling ssoAuthDone action then corresponding method of pending auth handler is called`() {
|
||||||
|
// Given
|
||||||
|
val devices = mockk<List<DeviceFullInfo>>()
|
||||||
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
|
justRun { fakePendingAuthHandler.instance.ssoAuthDone() }
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(OtherSessionsAction.SsoAuthDone)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest.finish()
|
||||||
|
verifyAll {
|
||||||
|
fakePendingAuthHandler.instance.ssoAuthDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given password auth has been done when handling passwordAuthDone action then corresponding method of pending auth handler is called`() {
|
||||||
|
// Given
|
||||||
|
val devices = mockk<List<DeviceFullInfo>>()
|
||||||
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
|
justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) }
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(OtherSessionsAction.PasswordAuthDone(A_PASSWORD))
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest.finish()
|
||||||
|
verifyAll {
|
||||||
|
fakePendingAuthHandler.instance.passwordAuthDone(A_PASSWORD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given reAuth has been cancelled when handling reAuthCancelled action then corresponding method of pending auth handler is called`() {
|
||||||
|
// Given
|
||||||
|
val devices = mockk<List<DeviceFullInfo>>()
|
||||||
|
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
|
||||||
|
justRun { fakePendingAuthHandler.instance.reAuthCancelled() }
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(OtherSessionsAction.ReAuthCancelled)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest.finish()
|
||||||
|
verifyAll {
|
||||||
|
fakePendingAuthHandler.instance.reAuthCancelled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun givenGetDeviceFullInfoListReturns(
|
private fun givenGetDeviceFullInfoListReturns(
|
||||||
filterType: DeviceManagerFilterType,
|
filterType: DeviceManagerFilterType,
|
||||||
devices: List<DeviceFullInfo>,
|
devices: List<DeviceFullInfo>,
|
||||||
|
|
|
@ -20,18 +20,15 @@ import android.os.SystemClock
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.test.MavericksTestRule
|
import com.airbnb.mvrx.test.MavericksTestRule
|
||||||
import im.vector.app.R
|
|
||||||
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
|
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
|
||||||
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
|
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
|
import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
|
||||||
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
|
import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
|
||||||
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
|
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.SignoutSessionUseCase
|
|
||||||
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
||||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||||
import im.vector.app.test.fakes.FakePendingAuthHandler
|
import im.vector.app.test.fakes.FakePendingAuthHandler
|
||||||
import im.vector.app.test.fakes.FakeStringProvider
|
import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
|
||||||
import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase
|
import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase
|
||||||
import im.vector.app.test.fakes.FakeVerificationService
|
import im.vector.app.test.fakes.FakeVerificationService
|
||||||
import im.vector.app.test.test
|
import im.vector.app.test.test
|
||||||
|
@ -43,7 +40,6 @@ import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.runs
|
import io.mockk.runs
|
||||||
import io.mockk.slot
|
|
||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import io.mockk.verifyAll
|
import io.mockk.verifyAll
|
||||||
|
@ -53,19 +49,11 @@ import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
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.crypto.model.RoomEncryptionTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
|
|
||||||
private const val A_SESSION_ID_1 = "session-id-1"
|
private const val A_SESSION_ID_1 = "session-id-1"
|
||||||
private const val A_SESSION_ID_2 = "session-id-2"
|
private const val A_SESSION_ID_2 = "session-id-2"
|
||||||
private const val AUTH_ERROR_MESSAGE = "auth-error-message"
|
|
||||||
private const val AN_ERROR_MESSAGE = "error-message"
|
|
||||||
private const val A_PASSWORD = "password"
|
private const val A_PASSWORD = "password"
|
||||||
|
|
||||||
class SessionOverviewViewModelTest {
|
class SessionOverviewViewModelTest {
|
||||||
|
@ -81,22 +69,20 @@ class SessionOverviewViewModelTest {
|
||||||
)
|
)
|
||||||
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>(relaxed = true)
|
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>(relaxed = true)
|
||||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||||
private val fakeStringProvider = FakeStringProvider()
|
|
||||||
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
|
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
|
||||||
private val signoutSessionUseCase = mockk<SignoutSessionUseCase>()
|
private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
|
||||||
private val interceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
|
private val interceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
|
||||||
private val fakePendingAuthHandler = FakePendingAuthHandler()
|
private val fakePendingAuthHandler = FakePendingAuthHandler()
|
||||||
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>()
|
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true)
|
||||||
private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase()
|
private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase()
|
||||||
private val fakeGetNotificationsStatusUseCase = mockk<GetNotificationsStatusUseCase>()
|
private val fakeGetNotificationsStatusUseCase = mockk<GetNotificationsStatusUseCase>()
|
||||||
private val notificationsStatus = NotificationsStatus.ENABLED
|
private val notificationsStatus = NotificationsStatus.ENABLED
|
||||||
|
|
||||||
private fun createViewModel() = SessionOverviewViewModel(
|
private fun createViewModel() = SessionOverviewViewModel(
|
||||||
initialState = SessionOverviewViewState(args),
|
initialState = SessionOverviewViewState(args),
|
||||||
stringProvider = fakeStringProvider.instance,
|
|
||||||
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
|
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
|
||||||
checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
|
checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
|
||||||
signoutSessionUseCase = signoutSessionUseCase,
|
signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
|
||||||
interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase,
|
interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase,
|
||||||
pendingAuthHandler = fakePendingAuthHandler.instance,
|
pendingAuthHandler = fakePendingAuthHandler.instance,
|
||||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||||
|
@ -115,11 +101,50 @@ class SessionOverviewViewModelTest {
|
||||||
every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus)
|
every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun givenVerificationService(): FakeVerificationService {
|
||||||
|
val fakeVerificationService = fakeActiveSessionHolder
|
||||||
|
.fakeSession
|
||||||
|
.fakeCryptoService
|
||||||
|
.fakeVerificationService
|
||||||
|
fakeVerificationService.givenAddListenerSucceeds()
|
||||||
|
fakeVerificationService.givenRemoveListenerSucceeds()
|
||||||
|
return fakeVerificationService
|
||||||
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
unmockkAll()
|
unmockkAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given the viewModel when initializing it then verification listener is added`() {
|
||||||
|
// Given
|
||||||
|
val fakeVerificationService = givenVerificationService()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verify {
|
||||||
|
fakeVerificationService.addListener(viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given the viewModel when clearing it then verification listener is removed`() {
|
||||||
|
// Given
|
||||||
|
val fakeVerificationService = givenVerificationService()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.onCleared()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verify {
|
||||||
|
fakeVerificationService.removeListener(viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given the viewModel has been initialized then pushers are refreshed`() {
|
fun `given the viewModel has been initialized then pushers are refreshed`() {
|
||||||
createViewModel()
|
createViewModel()
|
||||||
|
@ -223,8 +248,7 @@ class SessionOverviewViewModelTest {
|
||||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||||
every { deviceFullInfo.isCurrentDevice } returns false
|
every { deviceFullInfo.isCurrentDevice } returns false
|
||||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||||
givenSignoutSuccess(A_SESSION_ID_1)
|
fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_SESSION_ID_1))
|
||||||
every { refreshDevicesUseCase.execute() } just runs
|
|
||||||
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
||||||
givenCurrentSessionIsTrusted()
|
givenCurrentSessionIsTrusted()
|
||||||
val expectedViewState = SessionOverviewViewState(
|
val expectedViewState = SessionOverviewViewState(
|
||||||
|
@ -254,41 +278,6 @@ class SessionOverviewViewModelTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `given another session and server error during signout when handling signout action then signout process is performed`() {
|
|
||||||
// Given
|
|
||||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
|
||||||
every { deviceFullInfo.isCurrentDevice } returns false
|
|
||||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
|
||||||
val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED)
|
|
||||||
givenSignoutError(A_SESSION_ID_1, serverError)
|
|
||||||
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
|
||||||
givenCurrentSessionIsTrusted()
|
|
||||||
val expectedViewState = SessionOverviewViewState(
|
|
||||||
deviceId = A_SESSION_ID_1,
|
|
||||||
isCurrentSessionTrusted = true,
|
|
||||||
deviceInfo = Success(deviceFullInfo),
|
|
||||||
isLoading = false,
|
|
||||||
notificationsStatus = notificationsStatus,
|
|
||||||
)
|
|
||||||
fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE)
|
|
||||||
|
|
||||||
// When
|
|
||||||
val viewModel = createViewModel()
|
|
||||||
val viewModelTest = viewModel.test()
|
|
||||||
viewModel.handle(signoutAction)
|
|
||||||
|
|
||||||
// Then
|
|
||||||
viewModelTest
|
|
||||||
.assertStatesChanges(
|
|
||||||
expectedViewState,
|
|
||||||
{ copy(isLoading = true) },
|
|
||||||
{ copy(isLoading = false) }
|
|
||||||
)
|
|
||||||
.assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE }
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given another session and unexpected error during signout when handling signout action then signout process is performed`() {
|
fun `given another session and unexpected error during signout when handling signout action then signout process is performed`() {
|
||||||
// Given
|
// Given
|
||||||
|
@ -296,7 +285,7 @@ class SessionOverviewViewModelTest {
|
||||||
every { deviceFullInfo.isCurrentDevice } returns false
|
every { deviceFullInfo.isCurrentDevice } returns false
|
||||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||||
val error = Exception()
|
val error = Exception()
|
||||||
givenSignoutError(A_SESSION_ID_1, error)
|
fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_SESSION_ID_1), error)
|
||||||
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
||||||
givenCurrentSessionIsTrusted()
|
givenCurrentSessionIsTrusted()
|
||||||
val expectedViewState = SessionOverviewViewState(
|
val expectedViewState = SessionOverviewViewState(
|
||||||
|
@ -306,7 +295,6 @@ class SessionOverviewViewModelTest {
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
notificationsStatus = notificationsStatus,
|
notificationsStatus = notificationsStatus,
|
||||||
)
|
)
|
||||||
fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE)
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -320,7 +308,7 @@ class SessionOverviewViewModelTest {
|
||||||
{ copy(isLoading = true) },
|
{ copy(isLoading = true) },
|
||||||
{ copy(isLoading = false) }
|
{ copy(isLoading = false) }
|
||||||
)
|
)
|
||||||
.assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE }
|
.assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error == error }
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,7 +318,7 @@ class SessionOverviewViewModelTest {
|
||||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||||
every { deviceFullInfo.isCurrentDevice } returns false
|
every { deviceFullInfo.isCurrentDevice } returns false
|
||||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||||
val reAuthNeeded = givenSignoutReAuthNeeded(A_SESSION_ID_1)
|
val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_SESSION_ID_1))
|
||||||
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
||||||
givenCurrentSessionIsTrusted()
|
givenCurrentSessionIsTrusted()
|
||||||
val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
|
val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
|
||||||
|
@ -415,53 +403,6 @@ class SessionOverviewViewModelTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenSignoutSuccess(deviceId: String) {
|
|
||||||
val interceptor = slot<UserInteractiveAuthInterceptor>()
|
|
||||||
val flowResponse = mockk<RegistrationFlowResponse>()
|
|
||||||
val errorCode = "errorCode"
|
|
||||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
|
||||||
every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed
|
|
||||||
coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers {
|
|
||||||
secondArg<UserInteractiveAuthInterceptor>().performStage(flowResponse, errorCode, promise)
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun givenSignoutReAuthNeeded(deviceId: String): SignoutSessionResult.ReAuthNeeded {
|
|
||||||
val interceptor = slot<UserInteractiveAuthInterceptor>()
|
|
||||||
val flowResponse = mockk<RegistrationFlowResponse>()
|
|
||||||
every { flowResponse.session } returns A_SESSION_ID_1
|
|
||||||
val errorCode = "errorCode"
|
|
||||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
|
||||||
val reAuthNeeded = SignoutSessionResult.ReAuthNeeded(
|
|
||||||
pendingAuth = mockk(),
|
|
||||||
uiaContinuation = promise,
|
|
||||||
flowResponse = flowResponse,
|
|
||||||
errCode = errorCode,
|
|
||||||
)
|
|
||||||
every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded
|
|
||||||
coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers {
|
|
||||||
secondArg<UserInteractiveAuthInterceptor>().performStage(flowResponse, errorCode, promise)
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
return reAuthNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun givenSignoutError(deviceId: String, error: Throwable) {
|
|
||||||
coEvery { signoutSessionUseCase.execute(deviceId, any()) } returns Result.failure(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun givenVerificationService(): FakeVerificationService {
|
|
||||||
val fakeVerificationService = fakeActiveSessionHolder
|
|
||||||
.fakeSession
|
|
||||||
.fakeCryptoService
|
|
||||||
.fakeVerificationService
|
|
||||||
fakeVerificationService.givenAddListenerSucceeds()
|
|
||||||
fakeVerificationService.givenRemoveListenerSucceeds()
|
|
||||||
return fakeVerificationService
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun givenCurrentSessionIsTrusted() {
|
private fun givenCurrentSessionIsTrusted() {
|
||||||
fakeActiveSessionHolder.fakeSession.givenSessionId(A_SESSION_ID_2)
|
fakeActiveSessionHolder.fakeSession.givenSessionId(A_SESSION_ID_2)
|
||||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||||
|
|
|
@ -24,8 +24,8 @@ import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.runs
|
import io.mockk.runs
|
||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
|
import org.amshove.kluent.shouldBe
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.amshove.kluent.shouldBeInstanceOf
|
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -63,7 +63,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and success is returned`() {
|
fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and null is returned`() {
|
||||||
// Given
|
// Given
|
||||||
val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID)
|
val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID)
|
||||||
fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
|
fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
|
||||||
|
@ -84,7 +84,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
result shouldBeInstanceOf (SignoutSessionResult.Completed::class)
|
result shouldBe null
|
||||||
every {
|
every {
|
||||||
promise.resume(expectedAuth)
|
promise.resume(expectedAuth)
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
|
||||||
fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
|
fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
|
||||||
val errorCode = AN_ERROR_CODE
|
val errorCode = AN_ERROR_CODE
|
||||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
val promise = mockk<Continuation<UIABaseAuth>>()
|
||||||
val expectedResult = SignoutSessionResult.ReAuthNeeded(
|
val expectedResult = SignoutSessionsReAuthNeeded(
|
||||||
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
|
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
|
||||||
uiaContinuation = promise,
|
uiaContinuation = promise,
|
||||||
flowResponse = registrationFlowResponse,
|
flowResponse = registrationFlowResponse,
|
||||||
|
@ -122,7 +122,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
|
||||||
fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
|
fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
|
||||||
val errorCode: String? = null
|
val errorCode: String? = null
|
||||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
val promise = mockk<Continuation<UIABaseAuth>>()
|
||||||
val expectedResult = SignoutSessionResult.ReAuthNeeded(
|
val expectedResult = SignoutSessionsReAuthNeeded(
|
||||||
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
|
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
|
||||||
uiaContinuation = promise,
|
uiaContinuation = promise,
|
||||||
flowResponse = registrationFlowResponse,
|
flowResponse = registrationFlowResponse,
|
||||||
|
@ -147,7 +147,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
|
||||||
fakeReAuthHelper.givenStoredPassword(null)
|
fakeReAuthHelper.givenStoredPassword(null)
|
||||||
val errorCode: String? = null
|
val errorCode: String? = null
|
||||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
val promise = mockk<Continuation<UIABaseAuth>>()
|
||||||
val expectedResult = SignoutSessionResult.ReAuthNeeded(
|
val expectedResult = SignoutSessionsReAuthNeeded(
|
||||||
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
|
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
|
||||||
uiaContinuation = promise,
|
uiaContinuation = promise,
|
||||||
flowResponse = registrationFlowResponse,
|
flowResponse = registrationFlowResponse,
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.test.fakes.FakeActiveSessionHolder
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.amshove.kluent.shouldBe
|
|
||||||
import org.junit.Test
|
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
|
||||||
|
|
||||||
private const val A_DEVICE_ID = "device-id"
|
|
||||||
|
|
||||||
class SignoutSessionUseCaseTest {
|
|
||||||
|
|
||||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
|
||||||
|
|
||||||
private val signoutSessionUseCase = SignoutSessionUseCase(
|
|
||||||
activeSessionHolder = fakeActiveSessionHolder.instance
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `given a device id when signing out with success then success result is returned`() = runTest {
|
|
||||||
// Given
|
|
||||||
val interceptor = givenAuthInterceptor()
|
|
||||||
fakeActiveSessionHolder.fakeSession
|
|
||||||
.fakeCryptoService
|
|
||||||
.givenDeleteDeviceSucceeds(A_DEVICE_ID)
|
|
||||||
|
|
||||||
// When
|
|
||||||
val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor)
|
|
||||||
|
|
||||||
// Then
|
|
||||||
result.isSuccess shouldBe true
|
|
||||||
every {
|
|
||||||
fakeActiveSessionHolder.fakeSession
|
|
||||||
.fakeCryptoService
|
|
||||||
.deleteDevice(A_DEVICE_ID, interceptor, any())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `given a device id when signing out with error then failure result is returned`() = runTest {
|
|
||||||
// Given
|
|
||||||
val interceptor = givenAuthInterceptor()
|
|
||||||
val error = mockk<Exception>()
|
|
||||||
fakeActiveSessionHolder.fakeSession
|
|
||||||
.fakeCryptoService
|
|
||||||
.givenDeleteDeviceFailsWithError(A_DEVICE_ID, error)
|
|
||||||
|
|
||||||
// When
|
|
||||||
val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor)
|
|
||||||
|
|
||||||
// Then
|
|
||||||
result.isFailure shouldBe true
|
|
||||||
every {
|
|
||||||
fakeActiveSessionHolder.fakeSession
|
|
||||||
.fakeCryptoService
|
|
||||||
.deleteDevice(A_DEVICE_ID, interceptor, any())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun givenAuthInterceptor() = mockk<UserInteractiveAuthInterceptor>()
|
|
||||||
}
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* 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.test.fakes.FakeActiveSessionHolder
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBe
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_DEVICE_ID_1 = "device-id-1"
|
||||||
|
private const val A_DEVICE_ID_2 = "device-id-2"
|
||||||
|
|
||||||
|
class SignoutSessionsUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||||
|
private val fakeInterceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
|
||||||
|
|
||||||
|
private val signoutSessionsUseCase = SignoutSessionsUseCase(
|
||||||
|
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||||
|
interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a list of device ids when signing out with success then success result is returned`() = runTest {
|
||||||
|
// Given
|
||||||
|
val callback = givenOnReAuthCallback()
|
||||||
|
val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)
|
||||||
|
fakeActiveSessionHolder.fakeSession
|
||||||
|
.fakeCryptoService
|
||||||
|
.givenDeleteDevicesSucceeds(deviceIds)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = signoutSessionsUseCase.execute(deviceIds, callback)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result.isSuccess shouldBe true
|
||||||
|
verify {
|
||||||
|
fakeActiveSessionHolder.fakeSession
|
||||||
|
.fakeCryptoService
|
||||||
|
.deleteDevices(deviceIds, any(), any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a list of device ids when signing out with error then failure result is returned`() = runTest {
|
||||||
|
// Given
|
||||||
|
val interceptor = givenOnReAuthCallback()
|
||||||
|
val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)
|
||||||
|
val error = mockk<Exception>()
|
||||||
|
fakeActiveSessionHolder.fakeSession
|
||||||
|
.fakeCryptoService
|
||||||
|
.givenDeleteDevicesFailsWithError(deviceIds, error)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = signoutSessionsUseCase.execute(deviceIds, interceptor)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result.isFailure shouldBe true
|
||||||
|
verify {
|
||||||
|
fakeActiveSessionHolder.fakeSession
|
||||||
|
.fakeCryptoService
|
||||||
|
.deleteDevices(deviceIds, any(), any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a list of device ids when signing out with reAuth needed then callback is called`() = runTest {
|
||||||
|
// Given
|
||||||
|
val callback = givenOnReAuthCallback()
|
||||||
|
val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)
|
||||||
|
fakeActiveSessionHolder.fakeSession
|
||||||
|
.fakeCryptoService
|
||||||
|
.givenDeleteDevicesNeedsUIAuth(deviceIds)
|
||||||
|
val reAuthNeeded = SignoutSessionsReAuthNeeded(
|
||||||
|
pendingAuth = mockk(),
|
||||||
|
uiaContinuation = mockk(),
|
||||||
|
flowResponse = mockk(),
|
||||||
|
errCode = "errorCode"
|
||||||
|
)
|
||||||
|
every { fakeInterceptSignoutFlowResponseUseCase.execute(any(), any(), any()) } returns reAuthNeeded
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = signoutSessionsUseCase.execute(deviceIds, callback)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result.isSuccess shouldBe true
|
||||||
|
verify {
|
||||||
|
fakeActiveSessionHolder.fakeSession
|
||||||
|
.fakeCryptoService
|
||||||
|
.deleteDevices(deviceIds, any(), any())
|
||||||
|
callback(reAuthNeeded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenOnReAuthCallback(): (SignoutSessionsReAuthNeeded) -> Unit = {}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
|
@ -70,16 +71,21 @@ class FakeCryptoService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun givenDeleteDeviceSucceeds(deviceId: String) {
|
fun givenDeleteDevicesSucceeds(deviceIds: List<String>) {
|
||||||
val matrixCallback = slot<MatrixCallback<Unit>>()
|
every { deleteDevices(deviceIds, any(), any()) } answers {
|
||||||
every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers {
|
|
||||||
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
|
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun givenDeleteDeviceFailsWithError(deviceId: String, error: Exception) {
|
fun givenDeleteDevicesNeedsUIAuth(deviceIds: List<String>) {
|
||||||
val matrixCallback = slot<MatrixCallback<Unit>>()
|
every { deleteDevices(deviceIds, any(), any()) } answers {
|
||||||
every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers {
|
secondArg<UserInteractiveAuthInterceptor>().performStage(mockk(), "", mockk())
|
||||||
|
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenDeleteDevicesFailsWithError(deviceIds: List<String>, error: Exception) {
|
||||||
|
every { deleteDevices(deviceIds, any(), any()) } answers {
|
||||||
thirdArg<MatrixCallback<Unit>>().onFailure(error)
|
thirdArg<MatrixCallback<Unit>>().onFailure(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
|
||||||
|
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
|
||||||
|
class FakeSignoutSessionsUseCase {
|
||||||
|
|
||||||
|
val instance = mockk<SignoutSessionsUseCase>()
|
||||||
|
|
||||||
|
fun givenSignoutSuccess(deviceIds: List<String>) {
|
||||||
|
coEvery { instance.execute(deviceIds, any()) } returns Result.success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenSignoutReAuthNeeded(deviceIds: List<String>): SignoutSessionsReAuthNeeded {
|
||||||
|
val flowResponse = mockk<RegistrationFlowResponse>()
|
||||||
|
every { flowResponse.session } returns "a-session-id"
|
||||||
|
val errorCode = "errorCode"
|
||||||
|
val reAuthNeeded = SignoutSessionsReAuthNeeded(
|
||||||
|
pendingAuth = mockk(),
|
||||||
|
uiaContinuation = mockk(),
|
||||||
|
flowResponse = flowResponse,
|
||||||
|
errCode = errorCode,
|
||||||
|
)
|
||||||
|
coEvery { instance.execute(deviceIds, any()) } coAnswers {
|
||||||
|
secondArg<(SignoutSessionsReAuthNeeded) -> Unit>().invoke(reAuthNeeded)
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reAuthNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenSignoutError(deviceIds: List<String>, error: Throwable) {
|
||||||
|
coEvery { instance.execute(deviceIds, any()) } returns Result.failure(error)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue