Adds Push Notification toggle to Device Manager (#7261)

* Adds push notifications switch

* Adds functionality to Push notification toggle

* Adds DefaultPushersServiceTest for togglePusher

* Adds DefaultTogglePusherTaskTest

* Adds SessionOverviewViewModelTest for toggling pusher

* Hides pusher toggle if there are no pushers of the device

* Adds changelog file

* Edits changelog file

* Fixes copyrights

* Unregisters checkedChangelistener in onDetachedFromWindow for switch view

* Fixes post merge errors

* Fixes legal copies

* Removes unused imports

* Fixes lint errors

* Fixes test errors

* Fixes error

* Fixes error

* Fixes error

* Fixes error

* Fixes error
This commit is contained in:
Eric Decanini 2022-10-10 19:21:34 -04:00 committed by GitHub
parent a096ff03c8
commit 2fe636e93b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 741 additions and 119 deletions

1
changelog.d/7261.wip Normal file
View File

@ -0,0 +1 @@
Adds pusher toggle setting to device manager v2

View File

@ -3304,6 +3304,8 @@
<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>
<string name="device_manager_push_notifications_title">Push notifications</string>
<string name="device_manager_push_notifications_description">Receive push notifications on this session.</string>
<string name="device_manager_session_details_session_name">Session name</string> <string name="device_manager_session_details_session_name">Session name</string>
<string name="device_manager_session_details_session_id">Session ID</string> <string name="device_manager_session_details_session_id">Session ID</string>
<string name="device_manager_session_details_session_last_activity">Last activity</string> <string name="device_manager_session_details_session_last_activity">Last activity</string>

View File

@ -6,4 +6,10 @@
<attr name="sessionOverviewEntryDescription" format="string" /> <attr name="sessionOverviewEntryDescription" format="string" />
</declare-styleable> </declare-styleable>
<declare-styleable name="SessionOverviewEntrySwitchView">
<attr name="sessionOverviewEntrySwitchTitle" format="string" />
<attr name="sessionOverviewEntrySwitchDescription" format="string" />
<attr name="sessionOverviewEntrySwitchEnabled" format="boolean" />
</declare-styleable>
</resources> </resources>

View File

@ -67,6 +67,14 @@ interface PushersService {
append: Boolean = true append: Boolean = true
) )
/**
* Enables or disables a registered pusher.
*
* @param pusher The pusher being toggled
* @param enable Whether the pusher should be enabled or disabled
*/
suspend fun togglePusher(pusher: Pusher, enable: Boolean)
/** /**
* Directly ask the push gateway to send a push to this device. * Directly ask the push gateway to send a push to this device.
* If successful, the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId. * If successful, the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId.

View File

@ -42,6 +42,7 @@ internal class DefaultPushersService @Inject constructor(
private val getPusherTask: GetPushersTask, private val getPusherTask: GetPushersTask,
private val pushGatewayNotifyTask: PushGatewayNotifyTask, private val pushGatewayNotifyTask: PushGatewayNotifyTask,
private val addPusherTask: AddPusherTask, private val addPusherTask: AddPusherTask,
private val togglePusherTask: TogglePusherTask,
private val removePusherTask: RemovePusherTask, private val removePusherTask: RemovePusherTask,
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : PushersService { ) : PushersService {
@ -108,6 +109,24 @@ internal class DefaultPushersService @Inject constructor(
) )
} }
override suspend fun togglePusher(pusher: Pusher, enable: Boolean) {
togglePusherTask.execute(TogglePusherTask.Params(pusher.toJsonPusher(), enable))
}
private fun Pusher.toJsonPusher() = JsonPusher(
pushKey = pushKey,
kind = kind,
appId = appId,
appDisplayName = appDisplayName,
deviceDisplayName = deviceDisplayName,
profileTag = profileTag,
lang = lang,
data = JsonPusherData(data.url, data.format),
append = false,
enabled = enabled,
deviceId = deviceId,
)
private fun enqueueAddPusher(pusher: JsonPusher): UUID { private fun enqueueAddPusher(pusher: JsonPusher): UUID {
val params = AddPusherWorker.Params(sessionId, pusher) val params = AddPusherWorker.Params(sessionId, pusher)
val request = workManagerProvider.matrixOneTimeWorkRequestBuilder<AddPusherWorker>() val request = workManagerProvider.matrixOneTimeWorkRequestBuilder<AddPusherWorker>()

View File

@ -68,6 +68,9 @@ internal abstract class PushersModule {
@Binds @Binds
abstract fun bindAddPusherTask(task: DefaultAddPusherTask): AddPusherTask abstract fun bindAddPusherTask(task: DefaultAddPusherTask): AddPusherTask
@Binds
abstract fun bindTogglePusherTask(task: DefaultTogglePusherTask): TogglePusherTask
@Binds @Binds
abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2021 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.session.pushers
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.internal.database.model.PusherEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.RequestExecutor
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
internal interface TogglePusherTask : Task<TogglePusherTask.Params, Unit> {
data class Params(val pusher: JsonPusher, val enable: Boolean)
}
internal class DefaultTogglePusherTask @Inject constructor(
private val pushersAPI: PushersAPI,
@SessionDatabase private val monarchy: Monarchy,
private val requestExecutor: RequestExecutor,
private val globalErrorReceiver: GlobalErrorReceiver
) : TogglePusherTask {
override suspend fun execute(params: TogglePusherTask.Params) {
val pusher = params.pusher.copy(enabled = params.enable)
requestExecutor.executeRequest(globalErrorReceiver) {
pushersAPI.setPusher(pusher)
}
monarchy.awaitTransaction { realm ->
val entity = PusherEntity.where(realm, params.pusher.pushKey).findFirst()
entity?.apply { enabled = params.enable }?.let { realm.insertOrUpdate(it) }
}
}
}

View File

@ -0,0 +1,66 @@
/*
* 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.session.pushers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.android.sdk.test.fakes.FakeAddPusherTask
import org.matrix.android.sdk.test.fakes.FakeGetPushersTask
import org.matrix.android.sdk.test.fakes.FakeMonarchy
import org.matrix.android.sdk.test.fakes.FakeRemovePusherTask
import org.matrix.android.sdk.test.fakes.FakeTaskExecutor
import org.matrix.android.sdk.test.fakes.FakeTogglePusherTask
import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider
import org.matrix.android.sdk.test.fakes.internal.FakePushGatewayNotifyTask
import org.matrix.android.sdk.test.fixtures.PusherFixture
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultPushersServiceTest {
private val workManagerProvider = FakeWorkManagerProvider()
private val monarchy = FakeMonarchy()
private val sessionId = ""
private val getPushersTask = FakeGetPushersTask()
private val pushGatewayNotifyTask = FakePushGatewayNotifyTask()
private val addPusherTask = FakeAddPusherTask()
private val togglePusherTask = FakeTogglePusherTask()
private val removePusherTask = FakeRemovePusherTask()
private val taskExecutor = FakeTaskExecutor()
private val pushersService = DefaultPushersService(
workManagerProvider.instance,
monarchy.instance,
sessionId,
getPushersTask,
pushGatewayNotifyTask,
addPusherTask,
togglePusherTask,
removePusherTask,
taskExecutor.instance,
)
@Test
fun `when togglePusher, then execute task`() = runTest {
val pusher = PusherFixture.aPusher()
val enable = true
pushersService.togglePusher(pusher, enable)
togglePusherTask.verifyExecution(pusher, enable)
}
}

View File

@ -0,0 +1,64 @@
/*
* 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.session.pushers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.matrix.android.sdk.internal.database.model.PusherEntity
import org.matrix.android.sdk.internal.database.model.PusherEntityFields
import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
import org.matrix.android.sdk.test.fakes.FakeMonarchy
import org.matrix.android.sdk.test.fakes.FakePushersAPI
import org.matrix.android.sdk.test.fakes.FakeRequestExecutor
import org.matrix.android.sdk.test.fakes.givenEqualTo
import org.matrix.android.sdk.test.fakes.givenFindFirst
import org.matrix.android.sdk.test.fixtures.JsonPusherFixture.aJsonPusher
import org.matrix.android.sdk.test.fixtures.PusherEntityFixture.aPusherEntity
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultTogglePusherTaskTest {
private val pushersAPI = FakePushersAPI()
private val monarchy = FakeMonarchy()
private val requestExecutor = FakeRequestExecutor()
private val globalErrorReceiver = FakeGlobalErrorReceiver()
private val togglePusherTask = DefaultTogglePusherTask(pushersAPI, monarchy.instance, requestExecutor, globalErrorReceiver)
@Test
fun `execution toggles enable on both local and remote`() = runTest {
val jsonPusher = aJsonPusher(enabled = false)
val params = TogglePusherTask.Params(aJsonPusher(), true)
val pusherEntity = aPusherEntity(enabled = false)
monarchy.givenWhere<PusherEntity>()
.givenEqualTo(PusherEntityFields.PUSH_KEY, jsonPusher.pushKey)
.givenFindFirst(pusherEntity)
togglePusherTask.execute(params)
val expectedPayload = jsonPusher.copy(enabled = true)
pushersAPI.verifySetPusher(expectedPayload)
monarchy.verifyInsertOrUpdate<PusherEntity> {
withArg { actual ->
actual.enabled shouldBeEqualTo true
}
}
}
}

View File

@ -0,0 +1,22 @@
/*
* 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.test.fakes
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.pushers.AddPusherTask
class FakeAddPusherTask : AddPusherTask by mockk()

View File

@ -0,0 +1,22 @@
/*
* 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.test.fakes
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.pushers.GetPushersTask
class FakeGetPushersTask : GetPushersTask by mockk()

View File

@ -0,0 +1,22 @@
/*
* 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.test.fakes
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.pushers.RemovePusherTask
class FakeRemovePusherTask : RemovePusherTask by mockk()

View File

@ -0,0 +1,25 @@
/*
* 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.test.fakes
import io.mockk.mockk
import org.matrix.android.sdk.internal.task.TaskExecutor
internal class FakeTaskExecutor {
val instance: TaskExecutor = mockk()
}

View File

@ -0,0 +1,35 @@
/*
* 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.test.fakes
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.slot
import org.amshove.kluent.shouldBeEqualTo
import org.matrix.android.sdk.api.session.pushers.Pusher
import org.matrix.android.sdk.internal.session.pushers.TogglePusherTask
class FakeTogglePusherTask : TogglePusherTask by mockk(relaxed = true) {
fun verifyExecution(pusher: Pusher, enable: Boolean) {
val slot = slot<TogglePusherTask.Params>()
coVerify { execute(capture(slot)) }
val params = slot.captured
params.pusher.pushKey shouldBeEqualTo pusher.pushKey
params.enable shouldBeEqualTo enable
}
}

View File

@ -0,0 +1,22 @@
/*
* 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.test.fakes.internal
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.pushers.gateway.PushGatewayNotifyTask
class FakePushGatewayNotifyTask : PushGatewayNotifyTask by mockk()

View File

@ -0,0 +1,50 @@
/*
* 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.test.fixtures
import org.matrix.android.sdk.api.session.pushers.Pusher
import org.matrix.android.sdk.api.session.pushers.PusherData
import org.matrix.android.sdk.api.session.pushers.PusherState
object PusherFixture {
fun aPusher(
pushKey: String = "",
kind: String = "",
appId: String = "",
appDisplayName: String? = "",
deviceDisplayName: String? = "",
profileTag: String? = null,
lang: String? = "",
data: PusherData = PusherData("f.o/_matrix/push/v1/notify", ""),
enabled: Boolean = true,
deviceId: String? = "",
state: PusherState = PusherState.REGISTERED,
) = Pusher(
pushKey,
kind,
appId,
appDisplayName,
deviceDisplayName,
profileTag,
lang,
data,
enabled,
deviceId,
state,
)
}

View File

@ -296,6 +296,7 @@ dependencies {
// Plant Timber tree for test // Plant Timber tree for test
testImplementation libs.tests.timberJunitRule testImplementation libs.tests.timberJunitRule
testImplementation libs.airbnb.mavericksTesting testImplementation libs.airbnb.mavericksTesting
testImplementation libs.androidx.coreTesting
testImplementation(libs.jetbrains.coroutinesTest) { testImplementation(libs.jetbrains.coroutinesTest) {
exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
} }

View File

@ -19,9 +19,14 @@ package im.vector.app.features.settings.devices.v2.overview
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed class SessionOverviewAction : VectorViewModelAction { sealed class SessionOverviewAction : VectorViewModelAction {
object VerifySession : SessionOverviewAction() object VerifySession : SessionOverviewAction()
object SignoutOtherSession : SessionOverviewAction() object SignoutOtherSession : SessionOverviewAction()
object SsoAuthDone : SessionOverviewAction() object SsoAuthDone : SessionOverviewAction()
data class PasswordAuthDone(val password: String) : SessionOverviewAction() data class PasswordAuthDone(val password: String) : SessionOverviewAction()
object ReAuthCancelled : SessionOverviewAction() object ReAuthCancelled : SessionOverviewAction()
data class TogglePushNotifications(
val deviceId: String,
val enabled: Boolean,
) : SessionOverviewAction()
} }

View File

@ -0,0 +1,86 @@
/*
* 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.overview
import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.CompoundButton
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.use
import im.vector.app.R
import im.vector.app.core.extensions.setAttributeBackground
import im.vector.app.databinding.ViewSessionOverviewEntrySwitchBinding
class SessionOverviewEntrySwitchView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val binding = ViewSessionOverviewEntrySwitchBinding.inflate(
LayoutInflater.from(context),
this
)
init {
initBackground()
context.obtainStyledAttributes(
attrs,
R.styleable.SessionOverviewEntrySwitchView,
0,
0
).use {
setTitle(it)
setDescription(it)
setSwitchedEnabled(it)
}
}
private fun initBackground() {
binding.root.setAttributeBackground(android.R.attr.selectableItemBackground)
}
private fun setTitle(typedArray: TypedArray) {
val title = typedArray.getString(R.styleable.SessionOverviewEntrySwitchView_sessionOverviewEntrySwitchTitle)
binding.sessionsOverviewEntryTitle.text = title
}
private fun setDescription(typedArray: TypedArray) {
val description = typedArray.getString(R.styleable.SessionOverviewEntrySwitchView_sessionOverviewEntrySwitchDescription)
binding.sessionsOverviewEntryDescription.text = description
}
private fun setSwitchedEnabled(typedArray: TypedArray) {
val enabled = typedArray.getBoolean(R.styleable.SessionOverviewEntrySwitchView_sessionOverviewEntrySwitchEnabled, true)
binding.sessionsOverviewEntrySwitch.isChecked = enabled
}
fun setChecked(checked: Boolean) {
binding.sessionsOverviewEntrySwitch.isChecked = checked
}
fun setOnCheckedChangeListener(listener: CompoundButton.OnCheckedChangeListener?) {
binding.sessionsOverviewEntrySwitch.setOnCheckedChangeListener(listener)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
binding.sessionsOverviewEntrySwitch.setOnCheckedChangeListener(null)
}
}

View File

@ -24,6 +24,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible 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
@ -41,12 +42,14 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.databinding.FragmentSessionOverviewBinding import im.vector.app.databinding.FragmentSessionOverviewBinding
import im.vector.app.features.auth.ReAuthActivity 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.settings.devices.v2.DeviceFullInfo
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.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
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.pushers.Pusher
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -174,9 +177,14 @@ class SessionOverviewFragment :
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
updateToolbar(state) updateToolbar(state)
updateEntryDetails(state.deviceId)
updateSessionInfo(state) updateSessionInfo(state)
updateLoading(state.isLoading) updateLoading(state.isLoading)
updatePushNotificationToggle(state.deviceId, state.pushers.invoke().orEmpty())
if (state.deviceInfo is Success) {
renderSessionInfo(state.isCurrentSessionTrusted, state.deviceInfo.invoke())
} else {
hideSessionInfo()
}
} }
private fun updateToolbar(viewState: SessionOverviewViewState) { private fun updateToolbar(viewState: SessionOverviewViewState) {
@ -189,12 +197,6 @@ class SessionOverviewFragment :
} }
} }
private fun updateEntryDetails(deviceId: String) {
views.sessionOverviewEntryDetails.setOnClickListener {
viewNavigator.goToSessionDetails(requireContext(), deviceId)
}
}
private fun updateSessionInfo(viewState: SessionOverviewViewState) { private fun updateSessionInfo(viewState: SessionOverviewViewState) {
if (viewState.deviceInfo is Success) { if (viewState.deviceInfo is Success) {
views.sessionOverviewInfo.isVisible = true views.sessionOverviewInfo.isVisible = true
@ -217,6 +219,35 @@ class SessionOverviewFragment :
} }
} }
private fun updatePushNotificationToggle(deviceId: String, pushers: List<Pusher>) {
views.sessionOverviewPushNotifications.apply {
if (pushers.isEmpty()) {
isVisible = false
} else {
val allPushersAreEnabled = pushers.all { it.enabled }
setOnCheckedChangeListener(null)
setChecked(allPushersAreEnabled)
post {
setOnCheckedChangeListener { _, isChecked ->
viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked))
}
}
}
}
}
private fun renderSessionInfo(isCurrentSession: Boolean, deviceFullInfo: DeviceFullInfo) {
views.sessionOverviewInfo.isVisible = true
val viewState = SessionInfoViewState(
isCurrentSession = isCurrentSession,
deviceFullInfo = deviceFullInfo,
isDetailsButtonVisible = false,
isLearnMoreLinkVisible = true,
isLastSeenDetailsVisible = true,
)
views.sessionOverviewInfo.render(viewState, dateFormatter, drawableProvider, colorProvider, stringProvider)
}
private fun updateLoading(isLoading: Boolean) { private fun updateLoading(isLoading: Boolean) {
if (isLoading) { if (isLoading) {
showLoading(null) showLoading(null)
@ -275,4 +306,8 @@ class SessionOverviewFragment :
) )
SessionLearnMoreBottomSheet.show(childFragmentManager, args) SessionLearnMoreBottomSheet.show(childFragmentManager, args)
} }
private fun hideSessionInfo() {
views.sessionOverviewInfo.isGone = true
}
} }

View File

@ -43,14 +43,17 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse 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.failure.Failure
import org.matrix.android.sdk.api.session.Session
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 org.matrix.android.sdk.flow.flow
import timber.log.Timber import timber.log.Timber
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
class SessionOverviewViewModel @AssistedInject constructor( class SessionOverviewViewModel @AssistedInject constructor(
@Assisted val initialState: SessionOverviewViewState, @Assisted val initialState: SessionOverviewViewState,
private val session: Session,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
@ -73,6 +76,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
init { init {
observeSessionInfo(initialState.deviceId) observeSessionInfo(initialState.deviceId)
observeCurrentSessionInfo() observeCurrentSessionInfo()
observePushers(initialState.deviceId)
} }
private fun observeSessionInfo(deviceId: String) { private fun observeSessionInfo(deviceId: String) {
@ -94,6 +98,13 @@ class SessionOverviewViewModel @AssistedInject constructor(
} }
} }
private fun observePushers(deviceId: String) {
session.flow()
.livePushers()
.map { it.filter { pusher -> pusher.deviceId == deviceId } }
.execute { copy(pushers = it) }
}
override fun handle(action: SessionOverviewAction) { override fun handle(action: SessionOverviewAction) {
when (action) { when (action) {
is SessionOverviewAction.VerifySession -> handleVerifySessionAction() is SessionOverviewAction.VerifySession -> handleVerifySessionAction()
@ -101,6 +112,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
SessionOverviewAction.SsoAuthDone -> handleSsoAuthDone() SessionOverviewAction.SsoAuthDone -> handleSsoAuthDone()
is SessionOverviewAction.PasswordAuthDone -> handlePasswordAuthDone(action) is SessionOverviewAction.PasswordAuthDone -> handlePasswordAuthDone(action)
SessionOverviewAction.ReAuthCancelled -> handleReAuthCancelled() SessionOverviewAction.ReAuthCancelled -> handleReAuthCancelled()
is SessionOverviewAction.TogglePushNotifications -> handleTogglePusherAction(action)
} }
} }
@ -198,4 +210,13 @@ class SessionOverviewViewModel @AssistedInject constructor(
private fun handleReAuthCancelled() { private fun handleReAuthCancelled() {
pendingAuthHandler.reAuthCancelled() pendingAuthHandler.reAuthCancelled()
} }
private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) {
viewModelScope.launch {
val devicePushers = awaitState().pushers.invoke()?.filter { it.deviceId == action.deviceId }
devicePushers?.forEach { pusher ->
session.pushersService().togglePusher(pusher, action.enabled)
}
}
}
} }

View File

@ -20,12 +20,14 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import org.matrix.android.sdk.api.session.pushers.Pusher
data class SessionOverviewViewState( data class SessionOverviewViewState(
val deviceId: String, val deviceId: String,
val isCurrentSessionTrusted: Boolean = false, val isCurrentSessionTrusted: Boolean = false,
val deviceInfo: Async<DeviceFullInfo> = Uninitialized, val deviceInfo: Async<DeviceFullInfo> = Uninitialized,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val pushers: Async<List<Pusher>> = Uninitialized,
) : MavericksState { ) : MavericksState {
constructor(args: SessionOverviewArgs) : this( constructor(args: SessionOverviewArgs) : this(
deviceId = args.deviceId deviceId = args.deviceId

View File

@ -31,6 +31,16 @@
app:sessionOverviewEntryDescription="@string/device_manager_session_details_description" app:sessionOverviewEntryDescription="@string/device_manager_session_details_description"
app:sessionOverviewEntryTitle="@string/device_manager_session_details_title" /> app:sessionOverviewEntryTitle="@string/device_manager_session_details_title" />
<im.vector.app.features.settings.devices.v2.overview.SessionOverviewEntrySwitchView
android:id="@+id/sessionOverviewPushNotifications"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sessionOverviewEntryDetails"
app:sessionOverviewEntrySwitchDescription="@string/device_manager_push_notifications_description"
app:sessionOverviewEntrySwitchTitle="@string/device_manager_push_notifications_title" />
<Button <Button
android:id="@+id/sessionOverviewSignout" android:id="@+id/sessionOverviewSignout"
style="@style/Widget.Vector.Button.Text.Destructive" style="@style/Widget.Vector.Button.Text.Destructive"

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<merge 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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<TextView
android:id="@+id/sessionsOverviewEntryTitle"
style="@style/TextAppearance.Vector.Subtitle.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
android:layout_marginTop="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Push notifications" />
<TextView
android:id="@+id/sessionsOverviewEntryDescription"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toEndOf="@id/sessionsOverviewEntryTitle"
app:layout_constraintStart_toStartOf="@id/sessionsOverviewEntryTitle"
app:layout_constraintTop_toBottomOf="@id/sessionsOverviewEntryTitle"
tools:text="Receive push notifications on this session." />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/sessionsOverviewEntrySwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<View
android:id="@+id/sessionsOverviewEntryDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="20dp"
android:background="@drawable/divider_horizontal"
app:layout_constraintEnd_toEndOf="@id/sessionsOverviewEntryTitle"
app:layout_constraintStart_toStartOf="@id/sessionsOverviewEntryTitle"
app:layout_constraintTop_toBottomOf="@id/sessionsOverviewEntryDescription" />
</merge>

View File

@ -17,9 +17,10 @@
package im.vector.app.features.settings.devices.v2.overview package im.vector.app.features.settings.devices.v2.overview
import android.os.SystemClock import android.os.SystemClock
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.airbnb.mvrx.Loading
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.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
@ -28,8 +29,10 @@ 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.FakeSession
import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeStringProvider
import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fakes.FakeVerificationService
import im.vector.app.test.fixtures.PusherFixture.aPusher
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
@ -52,10 +55,8 @@ import org.junit.Test
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.auth.registration.RegistrationFlowResponse 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 import kotlin.coroutines.Continuation
private const val A_SESSION_ID_1 = "session-id-1" private const val A_SESSION_ID_1 = "session-id-1"
@ -69,12 +70,16 @@ class SessionOverviewViewModelTest {
@get:Rule @get:Rule
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val args = SessionOverviewArgs( private val args = SessionOverviewArgs(
deviceId = A_SESSION_ID_1 deviceId = A_SESSION_ID_1
) )
private val fakeSession = FakeSession()
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>(relaxed = true)
private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val fakeStringProvider = FakeStringProvider() private val fakeStringProvider = FakeStringProvider()
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>()
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
private val signoutSessionUseCase = mockk<SignoutSessionUseCase>() private val signoutSessionUseCase = mockk<SignoutSessionUseCase>()
private val interceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>() private val interceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
@ -83,6 +88,7 @@ class SessionOverviewViewModelTest {
private fun createViewModel() = SessionOverviewViewModel( private fun createViewModel() = SessionOverviewViewModel(
initialState = SessionOverviewViewState(args), initialState = SessionOverviewViewState(args),
session = fakeSession,
stringProvider = fakeStringProvider.instance, stringProvider = fakeStringProvider.instance,
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
@ -108,8 +114,7 @@ class SessionOverviewViewModelTest {
} }
@Test @Test
fun `given the viewModel has been initialized then viewState is updated with session info and current session verification status`() { fun `given the viewModel has been initialized then viewState is updated with session info`() {
// Given
val deviceFullInfo = mockk<DeviceFullInfo>() val deviceFullInfo = mockk<DeviceFullInfo>()
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
givenCurrentSessionIsTrusted() givenCurrentSessionIsTrusted()
@ -117,12 +122,11 @@ class SessionOverviewViewModelTest {
deviceId = A_SESSION_ID_1, deviceId = A_SESSION_ID_1,
deviceInfo = Success(deviceFullInfo), deviceInfo = Success(deviceFullInfo),
isCurrentSessionTrusted = true, isCurrentSessionTrusted = true,
pushers = Loading(),
) )
// When
val viewModel = createViewModel() val viewModel = createViewModel()
// Then
viewModel.test() viewModel.test()
.assertLatestState { state -> state == expectedState } .assertLatestState { state -> state == expectedState }
.finish() .finish()
@ -199,110 +203,6 @@ class SessionOverviewViewModelTest {
.finish() .finish()
} }
@Test
fun `given another session and no reAuth is needed 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)
givenSignoutSuccess(A_SESSION_ID_1)
every { refreshDevicesUseCase.execute() } just runs
val signoutAction = SessionOverviewAction.SignoutOtherSession
givenCurrentSessionIsTrusted()
val expectedViewState = SessionOverviewViewState(
deviceId = A_SESSION_ID_1,
isCurrentSessionTrusted = true,
deviceInfo = Success(deviceFullInfo),
isLoading = false,
)
// 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.SignoutSuccess }
.finish()
verify {
refreshDevicesUseCase.execute()
}
}
@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,
)
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
fun `given another session and unexpected 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 error = Exception()
givenSignoutError(A_SESSION_ID_1, error)
val signoutAction = SessionOverviewAction.SignoutOtherSession
givenCurrentSessionIsTrusted()
val expectedViewState = SessionOverviewViewState(
deviceId = A_SESSION_ID_1,
isCurrentSessionTrusted = true,
deviceInfo = Success(deviceFullInfo),
isLoading = false,
)
fakeStringProvider.given(R.string.matrix_error, AN_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 == AN_ERROR_MESSAGE }
.finish()
}
@Test @Test
fun `given another session and reAuth is needed during signout when handling signout action then requestReAuth is sent and pending auth is stored`() { fun `given another session and reAuth is needed during signout when handling signout action then requestReAuth is sent and pending auth is stored`() {
// Given // Given
@ -447,4 +347,30 @@ class SessionOverviewViewModelTest {
every { deviceFullInfo.roomEncryptionTrustLevel } returns RoomEncryptionTrustLevel.Trusted every { deviceFullInfo.roomEncryptionTrustLevel } returns RoomEncryptionTrustLevel.Trusted
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_2) } returns flowOf(deviceFullInfo) every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_2) } returns flowOf(deviceFullInfo)
} }
@Test
fun `when viewModel init, then observe pushers and emit to state`() {
val pushers = listOf(aPusher(deviceId = A_SESSION_ID_1))
fakeSession.pushersService().givenPushersLive(pushers)
val viewModel = createViewModel()
viewModel.test()
.assertLatestState { state -> state.pushers.invoke() == pushers }
.finish()
}
@Test
fun `when handle TogglePushNotifications, then toggle enabled for device pushers`() {
val pushers = listOf(
aPusher(deviceId = A_SESSION_ID_1, enabled = false),
aPusher(deviceId = "another id", enabled = false)
)
fakeSession.pushersService().givenPushersLive(pushers)
val viewModel = createViewModel()
viewModel.handle(SessionOverviewAction.TogglePushNotifications(A_SESSION_ID_1, true))
fakeSession.pushersService().verifyOnlyTogglePusherCalled(pushers.first(), true)
}
} }

View File

@ -16,14 +16,30 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import androidx.lifecycle.liveData
import io.mockk.Ordering
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import io.mockk.verify import io.mockk.verify
import org.matrix.android.sdk.api.session.pushers.HttpPusher import org.matrix.android.sdk.api.session.pushers.HttpPusher
import org.matrix.android.sdk.api.session.pushers.Pusher
import org.matrix.android.sdk.api.session.pushers.PushersService import org.matrix.android.sdk.api.session.pushers.PushersService
class FakePushersService : PushersService by mockk(relaxed = true) { class FakePushersService : PushersService by mockk(relaxed = true) {
fun givenPushersLive(pushers: List<Pusher>) {
every { getPushersLive() } returns liveData { emit(pushers) }
}
fun verifyOnlyTogglePusherCalled(pusher: Pusher, enable: Boolean) {
coVerify(ordering = Ordering.ALL) {
getPushersLive() // verifies only getPushersLive and the following togglePusher was called
togglePusher(pusher, enable)
}
}
fun verifyEnqueueAddHttpPusher(): HttpPusher { fun verifyEnqueueAddHttpPusher(): HttpPusher {
val httpPusherSlot = slot<HttpPusher>() val httpPusherSlot = slot<HttpPusher>()
verify { enqueueAddHttpPusher(capture(httpPusherSlot)) } verify { enqueueAddHttpPusher(capture(httpPusherSlot)) }

View File

@ -0,0 +1,50 @@
/*
* 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.fixtures
import org.matrix.android.sdk.api.session.pushers.Pusher
import org.matrix.android.sdk.api.session.pushers.PusherData
import org.matrix.android.sdk.api.session.pushers.PusherState
object PusherFixture {
fun aPusher(
pushKey: String = "",
kind: String = "",
appId: String = "",
appDisplayName: String? = "",
deviceDisplayName: String? = "",
profileTag: String? = null,
lang: String? = "",
data: PusherData = PusherData("f.o/_matrix/push/v1/notify", ""),
enabled: Boolean = true,
deviceId: String? = "",
state: PusherState = PusherState.REGISTERED,
) = Pusher(
pushKey,
kind,
appId,
appDisplayName,
deviceDisplayName,
profileTag,
lang,
data,
enabled,
deviceId,
state,
)
}