Makes "Enable Notifications for this session" respond to enabled value in pusher (#7281)

* 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

* Links notification settings toggle to pusher service

* Adds changelog file

* Adds error handling to VectorSettingsNotificationPreferenceFragment

* Removes comment in FakePushersService

* Fixes post merge errors

* Fixes imports and improves string name

* Fixes legal copies

* Fixes kdoc punctuation

* Fixes string error

* Removes unused imports

* Fixes lint errors

* Fixes test errors

* Fixes test errors

* Fixes error

* Fixes error

* Fixes error

* Fixes error

* Fixes error

* Adds lost tests

* Adds PusherEntity migration

* Fixes session overview layout overlap

* Fixes switch being enabled by default

* Binds entire view to toggle switch
This commit is contained in:
Eric Decanini 2022-10-12 09:27:55 -04:00 committed by GitHub
parent cf9f30d95e
commit 9857fa6ca4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 303 additions and 2 deletions

1
changelog.d/7281.wip Normal file
View file

@ -0,0 +1 @@
Links "Enable Notifications for this session" setting to enabled value in pusher

View file

@ -1666,6 +1666,7 @@
<string name="create_new_room">Create New Room</string>
<string name="create_new_space">Create New Space</string>
<string name="error_no_network">No network. Please check your Internet connection.</string>
<string name="error_check_network">Something went wrong. Please check your network connection and try again.</string>
<string name="change_room_directory_network">"Change network"</string>
<string name="please_wait">"Please wait…"</string>
<string name="updating_your_data">Updating your data…</string>

View file

@ -54,6 +54,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
@ -62,7 +63,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 37L,
schemaVersion = 38L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
@ -109,5 +110,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 35) MigrateSessionTo035(realm).perform()
if (oldVersion < 36) MigrateSessionTo036(realm).perform()
if (oldVersion < 37) MigrateSessionTo037(realm).perform()
if (oldVersion < 38) MigrateSessionTo038(realm).perform()
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 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.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.PusherEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo038(realm: DynamicRealm) : RealmMigrator(realm, 38) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("PusherEntity")
?.addField(PusherEntityFields.ENABLED, Boolean::class.java)
?.addField(PusherEntityFields.DEVICE_ID, String::class.java)
?.transform { obj -> obj.set(PusherEntityFields.ENABLED, true) }
}
}

View file

@ -23,6 +23,7 @@ import im.vector.app.core.resources.AppNameProvider
import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.session.pushers.HttpPusher
import org.matrix.android.sdk.api.session.pushers.Pusher
import java.util.UUID
import javax.inject.Inject
import kotlin.math.abs
@ -90,6 +91,18 @@ class PushersManager @Inject constructor(
)
}
fun getPusherForCurrentSession(): Pusher? {
val session = activeSessionHolder.getSafeActiveSession() ?: return null
val deviceId = session.sessionParams.deviceId
return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
}
suspend fun togglePusherForCurrentSession(enable: Boolean) {
val session = activeSessionHolder.getSafeActiveSession() ?: return
val pusher = getPusherForCurrentSession() ?: return
session.pushersService().togglePusher(pusher, enable)
}
suspend fun unregisterEmailPusher(email: String) {
val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
currentSession.pushersService().removeEmailPusher(email)

View file

@ -49,6 +49,7 @@ class SessionOverviewEntrySwitchView @JvmOverloads constructor(
setTitle(it)
setDescription(it)
setSwitchedEnabled(it)
setClickListener()
}
}
@ -67,10 +68,16 @@ class SessionOverviewEntrySwitchView @JvmOverloads constructor(
}
private fun setSwitchedEnabled(typedArray: TypedArray) {
val enabled = typedArray.getBoolean(R.styleable.SessionOverviewEntrySwitchView_sessionOverviewEntrySwitchEnabled, true)
val enabled = typedArray.getBoolean(R.styleable.SessionOverviewEntrySwitchView_sessionOverviewEntrySwitchEnabled, false)
binding.sessionsOverviewEntrySwitch.isChecked = enabled
}
private fun setClickListener() {
binding.root.setOnClickListener {
setChecked(!binding.sessionsOverviewEntrySwitch.isChecked)
}
}
fun setChecked(checked: Boolean) {
binding.sessionsOverviewEntrySwitch.isChecked = checked
}

View file

@ -54,6 +54,7 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsBaseFragment
import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
@ -104,6 +105,10 @@ class VectorSettingsNotificationPreferenceFragment :
}
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let {
pushersManager.getPusherForCurrentSession()?.let { pusher ->
it.isChecked = pusher.enabled
}
it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
if (isChecked) {
unifiedPushHelper.register(requireActivity()) {
@ -117,6 +122,16 @@ class VectorSettingsNotificationPreferenceFragment :
}
findPreference<VectorPreference>(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)
?.summary = unifiedPushHelper.getCurrentDistributorName()
lifecycleScope.launch {
val result = runCatching {
pushersManager.togglePusherForCurrentSession(true)
}
result.exceptionOrNull()?.let { _ ->
Toast.makeText(context, R.string.error_check_network, Toast.LENGTH_SHORT).show()
it.isChecked = false
}
}
}
} else {
unifiedPushHelper.unregister(pushersManager)

View file

@ -24,8 +24,12 @@ import im.vector.app.test.fakes.FakeLocaleProvider
import im.vector.app.test.fakes.FakePushersService
import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.FakeStringProvider
import im.vector.app.test.fixtures.CredentialsFixture
import im.vector.app.test.fixtures.CryptoDeviceInfoFixture.aCryptoDeviceInfo
import im.vector.app.test.fixtures.PusherFixture
import im.vector.app.test.fixtures.SessionParamsFixture
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo
@ -82,4 +86,34 @@ class PushersManagerTest {
val httpPusher = pushersService.verifyEnqueueAddHttpPusher()
httpPusher shouldBeEqualTo expectedHttpPusher
}
@Test
fun `when getPusherForCurrentSession, then return pusher`() {
val deviceId = "device_id"
val sessionParams = SessionParamsFixture.aSessionParams(
credentials = CredentialsFixture.aCredentials(deviceId = deviceId)
)
session.givenSessionParams(sessionParams)
val expectedPusher = PusherFixture.aPusher(deviceId = deviceId)
pushersService.givenGetPushers(listOf(expectedPusher))
val pusher = pushersManager.getPusherForCurrentSession()
pusher shouldBeEqualTo expectedPusher
}
@Test
fun `when togglePusherForCurrentSession, then do service toggle pusher`() = runTest {
val deviceId = "device_id"
val sessionParams = SessionParamsFixture.aSessionParams(
credentials = CredentialsFixture.aCredentials(deviceId = deviceId)
)
session.givenSessionParams(sessionParams)
val pusher = PusherFixture.aPusher(deviceId = deviceId)
pushersService.givenGetPushers(listOf(pusher))
pushersManager.togglePusherForCurrentSession(true)
pushersService.verifyOnlyGetPushersAndTogglePusherCalled(pusher, true)
}
}

View file

@ -21,6 +21,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
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.RefreshDevicesUseCase
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
@ -55,8 +56,10 @@ 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.uia.DefaultBaseAuth
import javax.net.ssl.HttpsURLConnection
import kotlin.coroutines.Continuation
private const val A_SESSION_ID_1 = "session-id-1"
@ -203,6 +206,113 @@ class SessionOverviewViewModelTest {
.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,
pushers = Loading(),
)
// 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,
pushers = Loading(),
)
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,
pushers = Loading(),
)
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
fun `given another session and reAuth is needed during signout when handling signout action then requestReAuth is sent and pending auth is stored`() {
// Given

View file

@ -29,10 +29,21 @@ import org.matrix.android.sdk.api.session.pushers.PushersService
class FakePushersService : PushersService by mockk(relaxed = true) {
fun givenGetPushers(pushers: List<Pusher>) {
every { getPushers() } returns pushers
}
fun givenPushersLive(pushers: List<Pusher>) {
every { getPushersLive() } returns liveData { emit(pushers) }
}
fun verifyOnlyGetPushersAndTogglePusherCalled(pusher: Pusher, enable: Boolean) {
coVerify(ordering = Ordering.ALL) {
getPushers()
togglePusher(pusher, enable)
}
}
fun verifyOnlyTogglePusherCalled(pusher: Pusher, enable: Boolean) {
coVerify(ordering = Ordering.ALL) {
getPushersLive() // verifies only getPushersLive and the following togglePusher was called

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 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 im.vector.app.test.fixtures
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.DiscoveryInformation
object CredentialsFixture {
fun aCredentials(
userId: String = "",
accessToken: String = "",
refreshToken: String? = null,
homeServer: String? = null,
deviceId: String? = null,
discoveryInformation: DiscoveryInformation? = null,
) = Credentials(
userId,
accessToken,
refreshToken,
homeServer,
deviceId,
discoveryInformation,
)
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 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 im.vector.app.test.fixtures
import im.vector.app.test.fixtures.CredentialsFixture.aCredentials
import io.mockk.mockk
import org.matrix.android.sdk.api.auth.LoginType
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.SessionParams
object SessionParamsFixture {
fun aSessionParams(
credentials: Credentials = aCredentials(),
homeServerConnectionConfig: HomeServerConnectionConfig = mockk(relaxed = true),
isTokenValid: Boolean = false,
loginType: LoginType = LoginType.UNKNOWN,
) = SessionParams(
credentials,
homeServerConnectionConfig,
isTokenValid,
loginType,
)
}