Merge pull request #4228 from vector-im/feature/adm/suspending_add_pusher

Improved /settings/notifications push toggle error handling
This commit is contained in:
Benoit Marty 2021-10-12 14:50:01 +02:00 committed by GitHub
commit 36d2f8e46b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 610 additions and 195 deletions

2
changelog.d/4106.bugfix Normal file
View File

@ -0,0 +1,2 @@
Fixes push notification emails list not refreshing the first time seeing the notifications page.
Also improves the error handling in the email notification toggling by using synchronous flows instead of the WorkManager

View File

@ -130,6 +130,7 @@ dependencies {
// Database
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
kapt 'dk.ilios:realmfieldnameshelper:2.0.0'
// Work
@ -165,6 +166,8 @@ dependencies {
implementation libs.jetbrains.coroutinesAndroid
// Plant Timber tree for test
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
// Transitively required for mocking realm as monarchy doesn't expose Rx
testImplementation libs.rx.rxKotlin
kaptAndroidTest libs.dagger.daggerCompiler
androidTestImplementation libs.androidx.testCore

View File

@ -29,38 +29,19 @@ interface PushersService {
* Add a new HTTP pusher.
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set
*
* @param pushkey This is a unique identifier for this pusher. The value you should use for
* this is the routing or destination address information for the notification,
* for example, the APNS token for APNS or the Registration ID for GCM. If your
* notification client has no such concept, use any unique identifier. Max length, 512 chars.
* @param appId the application id
* This is a reverse-DNS style identifier for the application. It is recommended
* that this end with the platform, such that different platform versions get
* different app identifiers. Max length, 64 chars.
* @param profileTag This string determines which set of device specific rules this pusher executes.
* @param lang The preferred language for receiving notifications (e.g. "en" or "en-US").
* @param appDisplayName A human readable string that will allow the user to identify what application owns this pusher.
* @param deviceDisplayName A human readable string that will allow the user to identify what device owns this pusher.
* @param url The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify.
* @param append If true, the homeserver should add another pusher with the given pushkey and App ID in addition
* to any others with different user IDs. Otherwise, the homeserver must remove any other pushers
* with the same App ID and pushkey for different users.
* @param withEventIdOnly true to limit the push content to only id and not message content
* Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour
*
* @return A work request uuid. Can be used to listen to the status
* (LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(<UUID>))
* @throws [InvalidParameterException] if a parameter is not correct
*/
fun addHttpPusher(pushkey: String,
appId: String,
profileTag: String,
lang: String,
appDisplayName: String,
deviceDisplayName: String,
url: String,
append: Boolean,
withEventIdOnly: Boolean): UUID
suspend fun addHttpPusher(httpPusher: HttpPusher)
/**
* Enqueues a new HTTP pusher via the WorkManager API.
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set
*
* @return A work request uuid. Can be used to listen to the status
* (LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(<UUID>))
* @throws [InvalidParameterException] if a parameter is not correct
*/
fun enqueueAddHttpPusher(httpPusher: HttpPusher): UUID
/**
* Add a new Email pusher.
@ -75,16 +56,14 @@ interface PushersService {
* to any others with different user IDs. Otherwise, the homeserver must remove any other pushers
* with the same App ID and pushkey for different users. Typically We always want to append for
* email pushers since we don't want to stop other accounts notifying to the same email address.
* @return A work request uuid. Can be used to listen to the status
* (LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(<UUID>))
* @throws [InvalidParameterException] if a parameter is not correct
*/
fun addEmailPusher(email: String,
lang: String,
emailBranding: String,
appDisplayName: String,
deviceDisplayName: String,
append: Boolean = true): UUID
suspend fun addEmailPusher(email: String,
lang: String,
emailBranding: String,
appDisplayName: String,
deviceDisplayName: String,
append: Boolean = true)
/**
* Directly ask the push gateway to send a push to this device
@ -128,4 +107,61 @@ interface PushersService {
* Get the current pushers
*/
fun getPushers(): List<Pusher>
data class HttpPusher(
/**
* This is a unique identifier for this pusher. The value you should use for
* this is the routing or destination address information for the notification,
* for example, the APNS token for APNS or the Registration ID for GCM. If your
* notification client has no such concept, use any unique identifier. Max length, 512 chars.
*/
val pushkey: String,
/**
* The application id
* This is a reverse-DNS style identifier for the application. It is recommended
* that this end with the platform, such that different platform versions get
* different app identifiers. Max length, 64 chars.
*/
val appId: String,
/**
* This string determines which set of device specific rules this pusher executes.
*/
val profileTag: String,
/**
* The preferred language for receiving notifications (e.g. "en" or "en-US").
*/
val lang: String,
/**
* A human readable string that will allow the user to identify what application owns this pusher.
*/
val appDisplayName: String,
/**
* A human readable string that will allow the user to identify what device owns this pusher.
*/
val deviceDisplayName: String,
/**
* The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify.
*/
val url: String,
/**
* If true, the homeserver should add another pusher with the given pushkey and App ID in addition
* to any others with different user IDs. Otherwise, the homeserver must remove any other pushers
* with the same App ID and pushkey for different users.
*/
val append: Boolean,
/**
* true to limit the push content to only id and not message content
* Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour
*/
val withEventIdOnly: Boolean
)
}

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.extensions
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
@ -27,3 +28,25 @@ inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer:
inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
this.observe(owner, Observer { it?.run(observer) })
}
fun <T1, T2, R> combineLatest(source1: LiveData<T1>, source2: LiveData<T2>, mapper: (T1, T2) -> R): LiveData<R> {
val combined = MediatorLiveData<R>()
var source1Result: T1? = null
var source2Result: T2? = null
fun notify() {
if (source1Result != null && source2Result != null) {
combined.value = mapper(source1Result!!, source2Result!!)
}
}
combined.addSource(source1) {
source1Result = it
notify()
}
combined.addSource(source2) {
source2Result = it
notify()
}
return combined
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 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.network
import org.matrix.android.sdk.internal.network.executeRequest as internalExecuteRequest
internal interface RequestExecutor {
suspend fun <DATA> executeRequest(globalErrorReceiver: GlobalErrorReceiver?,
canRetry: Boolean = false,
maxDelayBeforeRetry: Long = 32_000L,
maxRetriesCount: Int = 4,
requestBlock: suspend () -> DATA): DATA
}
internal object DefaultRequestExecutor : RequestExecutor {
override suspend fun <DATA> executeRequest(globalErrorReceiver: GlobalErrorReceiver?,
canRetry: Boolean,
maxDelayBeforeRetry: Long,
maxRetriesCount: Int,
requestBlock: suspend () -> DATA): DATA {
return internalExecuteRequest(globalErrorReceiver, canRetry, maxDelayBeforeRetry, maxRetriesCount, requestBlock)
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 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.network
import dagger.Module
import dagger.Provides
@Module
internal object RequestModule {
@Provides
fun providesRequestExecutor(): RequestExecutor {
return DefaultRequestExecutor
}
}

View File

@ -30,6 +30,7 @@ import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessa
import org.matrix.android.sdk.internal.di.MatrixComponent
import org.matrix.android.sdk.internal.federation.FederationModule
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
import org.matrix.android.sdk.internal.network.RequestModule
import org.matrix.android.sdk.internal.session.account.AccountModule
import org.matrix.android.sdk.internal.session.cache.CacheModule
import org.matrix.android.sdk.internal.session.call.CallModule
@ -94,7 +95,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
CallModule::class,
SearchModule::class,
ThirdPartyModule::class,
SpaceModule::class
SpaceModule::class,
RequestModule::class
]
)
@SessionScope

View File

@ -0,0 +1,78 @@
/*
* 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.api.session.pushers.PusherState
import org.matrix.android.sdk.internal.database.mapper.toEntity
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 AddPusherTask : Task<AddPusherTask.Params, Unit> {
data class Params(val pusher: JsonPusher)
}
internal class DefaultAddPusherTask @Inject constructor(
private val pushersAPI: PushersAPI,
@SessionDatabase private val monarchy: Monarchy,
private val requestExecutor: RequestExecutor,
private val globalErrorReceiver: GlobalErrorReceiver
) : AddPusherTask {
override suspend fun execute(params: AddPusherTask.Params) {
val pusher = params.pusher
try {
setPusher(pusher)
} catch (error: Throwable) {
monarchy.awaitTransaction { realm ->
PusherEntity.where(realm, pusher.pushKey).findFirst()?.let {
it.state = PusherState.FAILED_TO_REGISTER
}
}
throw error
}
}
private suspend fun setPusher(pusher: JsonPusher) {
requestExecutor.executeRequest(globalErrorReceiver) {
pushersAPI.setPusher(pusher)
}
monarchy.awaitTransaction { realm ->
val echo = PusherEntity.where(realm, pusher.pushKey).findFirst()
if (echo == null) {
pusher.toEntity().also {
it.state = PusherState.REGISTERED
realm.insertOrUpdate(it)
}
} else {
echo.appDisplayName = pusher.appDisplayName
echo.appId = pusher.appId
echo.kind = pusher.kind
echo.lang = pusher.lang
echo.profileTag = pusher.profileTag
echo.data?.format = pusher.data?.format
echo.data?.url = pusher.data?.url
echo.state = PusherState.REGISTERED
}
}
}
}

View File

@ -18,17 +18,8 @@ package org.matrix.android.sdk.internal.session.pushers
import android.content.Context
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.pushers.PusherState
import org.matrix.android.sdk.internal.database.mapper.toEntity
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.executeRequest
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import javax.inject.Inject
@ -43,9 +34,7 @@ internal class AddPusherWorker(context: Context, params: WorkerParameters) :
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var pushersAPI: PushersAPI
@Inject @SessionDatabase lateinit var monarchy: Monarchy
@Inject lateinit var globalErrorReceiver: GlobalErrorReceiver
@Inject lateinit var addPusherTask: AddPusherTask
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
@ -58,20 +47,12 @@ internal class AddPusherWorker(context: Context, params: WorkerParameters) :
return Result.failure()
}
return try {
setPusher(pusher)
addPusherTask.execute(AddPusherTask.Params(pusher))
Result.success()
} catch (exception: Throwable) {
when (exception) {
is Failure.NetworkConnection -> Result.retry()
else -> {
monarchy.awaitTransaction { realm ->
PusherEntity.where(realm, pusher.pushKey).findFirst()?.let {
// update it
it.state = PusherState.FAILED_TO_REGISTER
}
}
Result.failure()
}
else -> Result.failure()
}
}
}
@ -79,29 +60,4 @@ internal class AddPusherWorker(context: Context, params: WorkerParameters) :
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
private suspend fun setPusher(pusher: JsonPusher) {
executeRequest(globalErrorReceiver) {
pushersAPI.setPusher(pusher)
}
monarchy.awaitTransaction { realm ->
val echo = PusherEntity.where(realm, pusher.pushKey).findFirst()
if (echo != null) {
// update it
echo.appDisplayName = pusher.appDisplayName
echo.appId = pusher.appId
echo.kind = pusher.kind
echo.lang = pusher.lang
echo.profileTag = pusher.profileTag
echo.data?.format = pusher.data?.format
echo.data?.url = pusher.data?.url
echo.state = PusherState.REGISTERED
} else {
pusher.toEntity().also {
it.state = PusherState.REGISTERED
realm.insertOrUpdate(it)
}
}
}
}
}

View File

@ -30,7 +30,6 @@ import org.matrix.android.sdk.internal.session.pushers.gateway.PushGatewayNotify
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import java.security.InvalidParameterException
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -41,6 +40,7 @@ internal class DefaultPushersService @Inject constructor(
@SessionId private val sessionId: String,
private val getPusherTask: GetPushersTask,
private val pushGatewayNotifyTask: PushGatewayNotifyTask,
private val addPusherTask: AddPusherTask,
private val removePusherTask: RemovePusherTask,
private val taskExecutor: TaskExecutor
) : PushersService {
@ -58,51 +58,48 @@ internal class DefaultPushersService @Inject constructor(
.executeBy(taskExecutor)
}
override fun addHttpPusher(pushkey: String,
appId: String,
profileTag: String,
lang: String,
appDisplayName: String,
deviceDisplayName: String,
url: String,
append: Boolean,
withEventIdOnly: Boolean
) = addPusher(
JsonPusher(
pushKey = pushkey,
kind = Pusher.KIND_HTTP,
appId = appId,
profileTag = profileTag,
lang = lang,
appDisplayName = appDisplayName,
deviceDisplayName = deviceDisplayName,
data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }),
append = append
)
override fun enqueueAddHttpPusher(httpPusher: PushersService.HttpPusher): UUID {
return enqueueAddPusher(httpPusher.toJsonPusher())
}
override suspend fun addHttpPusher(httpPusher: PushersService.HttpPusher) {
addPusherTask.execute(AddPusherTask.Params(httpPusher.toJsonPusher()))
}
private fun PushersService.HttpPusher.toJsonPusher() = JsonPusher(
pushKey = pushkey,
kind = "http",
appId = appId,
profileTag = profileTag,
lang = lang,
appDisplayName = appDisplayName,
deviceDisplayName = deviceDisplayName,
data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }),
append = append
)
override fun addEmailPusher(email: String,
lang: String,
emailBranding: String,
appDisplayName: String,
deviceDisplayName: String,
append: Boolean
) = addPusher(
JsonPusher(
pushKey = email,
kind = Pusher.KIND_EMAIL,
appId = Pusher.APP_ID_EMAIL,
profileTag = "",
lang = lang,
appDisplayName = appDisplayName,
deviceDisplayName = deviceDisplayName,
data = JsonPusherData(brand = emailBranding),
append = append
)
)
override suspend fun addEmailPusher(email: String,
lang: String,
emailBranding: String,
appDisplayName: String,
deviceDisplayName: String,
append: Boolean) {
addPusherTask.execute(
AddPusherTask.Params(JsonPusher(
pushKey = email,
kind = Pusher.KIND_EMAIL,
appId = Pusher.APP_ID_EMAIL,
profileTag = "",
lang = lang,
appDisplayName = appDisplayName,
deviceDisplayName = deviceDisplayName,
data = JsonPusherData(brand = emailBranding),
append = append
))
)
}
private fun addPusher(pusher: JsonPusher): UUID {
pusher.validateParameters()
private fun enqueueAddPusher(pusher: JsonPusher): UUID {
val params = AddPusherWorker.Params(sessionId, pusher)
val request = workManagerProvider.matrixOneTimeWorkRequestBuilder<AddPusherWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
@ -113,13 +110,6 @@ internal class DefaultPushersService @Inject constructor(
return request.id
}
private fun JsonPusher.validateParameters() {
// Do some parameter checks. It's ok to throw Exception, to inform developer of the problem
if (pushKey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars")
if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars")
data?.url?.let { url -> if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") }
}
override suspend fun removePusher(pusher: Pusher) {
removePusher(pusher.pushKey, pusher.appId)
}

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.pushers
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.di.SerializeNulls
import java.security.InvalidParameterException
/**
* Example:
@ -112,4 +113,11 @@ internal data class JsonPusher(
*/
@Json(name = "append")
val append: Boolean? = false
)
) {
init {
// Do some parameter checks. It's ok to throw Exception, to inform developer of the problem
if (pushKey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars")
if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars")
data?.url?.let { url -> if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") }
}
}

View File

@ -65,6 +65,9 @@ internal abstract class PushersModule {
@Binds
abstract fun bindSavePushRulesTask(task: DefaultSavePushRulesTask): SavePushRulesTask
@Binds
abstract fun bindAddPusherTask(task: DefaultAddPusherTask): AddPusherTask
@Binds
abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask

View File

@ -0,0 +1,103 @@
/*
* 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 kotlinx.coroutines.runBlocking
import org.amshove.kluent.internal.assertFailsWith
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.matrix.android.sdk.api.session.pushers.PusherState
import org.matrix.android.sdk.internal.database.model.PusherEntity
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 java.net.SocketException
private val A_JSON_PUSHER = JsonPusher(
pushKey = "push-key",
kind = "http",
appId = "m.email",
appDisplayName = "Element",
deviceDisplayName = null,
profileTag = "",
lang = "en-GB",
data = JsonPusherData(brand = "Element")
)
class DefaultAddPusherTaskTest {
private val pushersAPI = FakePushersAPI()
private val monarchy = FakeMonarchy()
private val addPusherTask = DefaultAddPusherTask(
pushersAPI = pushersAPI,
monarchy = monarchy.instance,
requestExecutor = FakeRequestExecutor(),
globalErrorReceiver = FakeGlobalErrorReceiver()
)
@Test
fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() {
monarchy.givenWhereReturns<PusherEntity>(result = null)
runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
pushersAPI.verifySetPusher(A_JSON_PUSHER)
monarchy.verifyInsertOrUpdate<PusherEntity> {
withArg { actual ->
actual.state shouldBeEqualTo PusherState.REGISTERED
}
}
}
@Test
fun `given a persisted pusher when adding Pusher then updates api and mutates persisted result with Registered state`() {
val realmResult = PusherEntity(appDisplayName = null)
monarchy.givenWhereReturns(result = realmResult)
runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
pushersAPI.verifySetPusher(A_JSON_PUSHER)
realmResult.appDisplayName shouldBeEqualTo A_JSON_PUSHER.appDisplayName
realmResult.state shouldBeEqualTo PusherState.REGISTERED
}
@Test
fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows error`() {
val realmResult = PusherEntity()
monarchy.givenWhereReturns(result = realmResult)
pushersAPI.givenSetPusherErrors(SocketException())
assertFailsWith<SocketException> {
runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
}
realmResult.state shouldBeEqualTo PusherState.FAILED_TO_REGISTER
}
@Test
fun `given no persisted push entity and SetPush API fails when adding Pusher then rethrows error`() {
monarchy.givenWhereReturns<PusherEntity>(result = null)
pushersAPI.givenSetPusherErrors(SocketException())
assertFailsWith<SocketException> {
runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
}
}
}

View File

@ -0,0 +1,26 @@
/*
* 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.test.fakes
import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
internal class FakeGlobalErrorReceiver : GlobalErrorReceiver {
override fun handleGlobalError(globalError: GlobalError) {
// do nothing
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.test.fakes
import com.zhuinden.monarchy.Monarchy
import io.mockk.MockKVerificationScope
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmQuery
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.util.awaitTransaction
internal class FakeMonarchy {
val instance = mockk<Monarchy>()
private val realm = mockk<Realm>(relaxed = true)
init {
mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
coEvery {
instance.awaitTransaction(any<suspend (Realm) -> Any>())
} coAnswers {
secondArg<suspend (Realm) -> Any>().invoke(realm)
}
}
inline fun <reified T : RealmModel> givenWhereReturns(result: T?) {
val queryResult = mockk<RealmQuery<T>>(relaxed = true)
every { queryResult.findFirst() } returns result
every { realm.where<T>() } returns queryResult
}
inline fun <reified T : RealmModel> verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) {
verify { realm.insertOrUpdate(verification()) }
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.test.fakes
import org.amshove.kluent.shouldBeEqualTo
import org.matrix.android.sdk.internal.session.pushers.GetPushersResponse
import org.matrix.android.sdk.internal.session.pushers.JsonPusher
import org.matrix.android.sdk.internal.session.pushers.PushersAPI
internal class FakePushersAPI : PushersAPI {
private var setRequestPayload: JsonPusher? = null
private var error: Throwable? = null
override suspend fun getPushers(): GetPushersResponse {
TODO("Not yet implemented")
}
override suspend fun setPusher(jsonPusher: JsonPusher) {
error?.let { throw it }
setRequestPayload = jsonPusher
}
fun verifySetPusher(payload: JsonPusher) {
this.setRequestPayload shouldBeEqualTo payload
}
fun givenSetPusherErrors(error: Throwable) {
this.error = error
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.test.fakes
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.RequestExecutor
internal class FakeRequestExecutor : RequestExecutor {
override suspend fun <DATA> executeRequest(globalErrorReceiver: GlobalErrorReceiver?,
canRetry: Boolean,
maxDelayBeforeRetry: Long,
maxRetriesCount: Int,
requestBlock: suspend () -> DATA): DATA {
return requestBlock()
}
}

View File

@ -57,7 +57,7 @@ class TestTokenRegistration @Inject constructor(private val context: AppCompatAc
stringProvider.getString(R.string.sas_error_unknown))
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_token_registration_quick_fix) {
override fun doFix() {
val workId = pushersManager.registerPusherWithFcmKey(fcmToken)
val workId = pushersManager.enqueueRegisterPusherWithFcmKey(fcmToken)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo ->
if (workInfo != null) {
if (workInfo.state == WorkInfo.State.SUCCEEDED) {

View File

@ -135,7 +135,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
Timber.tag(loggerTag.value).i("onNewToken: FCM Token has been updated")
FcmHelper.storeFcmToken(this, refreshedToken)
if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) {
pusherManager.registerPusherWithFcmKey(refreshedToken)
pusherManager.enqueueRegisterPusherWithFcmKey(refreshedToken)
}
}

View File

@ -75,7 +75,7 @@ object FcmHelper {
.addOnSuccessListener { token ->
storeFcmToken(activity, token)
if (registerPusher) {
pushersManager.registerPusherWithFcmKey(token)
pushersManager.enqueueRegisterPusherWithFcmKey(token)
}
}
.addOnFailureListener { e ->

View File

@ -21,6 +21,7 @@ import im.vector.app.core.di.ActiveSessionHolder
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.PushersService
import java.util.UUID
import javax.inject.Inject
import kotlin.math.abs
@ -44,24 +45,29 @@ class PushersManager @Inject constructor(
)
}
fun registerPusherWithFcmKey(pushKey: String): UUID {
fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID {
val currentSession = activeSessionHolder.getActiveSession()
val profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(currentSession.myUserId.hashCode())
return currentSession.addHttpPusher(
pushKey,
stringProvider.getString(R.string.pusher_app_id),
profileTag,
localeProvider.current().language,
appNameProvider.getAppName(),
currentSession.sessionParams.deviceId ?: "MOBILE",
stringProvider.getString(R.string.pusher_http_url),
append = false,
withEventIdOnly = true
)
return currentSession.enqueueAddHttpPusher(createHttpPusher(pushKey))
}
fun registerEmailForPush(email: String) {
suspend fun registerPusherWithFcmKey(pushKey: String) {
val currentSession = activeSessionHolder.getActiveSession()
currentSession.addHttpPusher(createHttpPusher(pushKey))
}
private fun createHttpPusher(pushKey: String) = PushersService.HttpPusher(
pushKey,
stringProvider.getString(R.string.pusher_app_id),
profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()),
localeProvider.current().language,
appNameProvider.getAppName(),
activeSessionHolder.getActiveSession().sessionParams.deviceId ?: "MOBILE",
stringProvider.getString(R.string.pusher_http_url),
append = false,
withEventIdOnly = true
)
suspend fun registerEmailForPush(email: String) {
val currentSession = activeSessionHolder.getActiveSession()
val appName = appNameProvider.getAppName()
currentSession.addEmailPusher(

View File

@ -54,6 +54,7 @@ import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.pushers.Pusher
import org.matrix.android.sdk.internal.extensions.combineLatest
import javax.inject.Inject
// Referenced in vector_settings_preferences_root.xml
@ -85,6 +86,21 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
(pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel
}
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let {
it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
if (isChecked) {
FcmHelper.getFcmToken(requireContext())?.let {
pushManager.registerPusherWithFcmKey(it)
}
} else {
FcmHelper.getFcmToken(requireContext())?.let {
pushManager.unregisterPusher(it)
session.refreshPushers()
}
}
}
}
findPreference<VectorPreference>(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let {
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val initialMode = vectorPreferences.getFdroidSyncBackgroundMode()
@ -324,46 +340,16 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) {
VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY -> {
updateEnabledForDevice(preference)
true
}
VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY -> {
VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY -> {
updateEnabledForAccount(preference)
true
}
else -> {
else -> {
return super.onPreferenceTreeClick(preference)
}
}
}
private fun updateEnabledForDevice(preference: Preference?) {
val switchPref = preference as SwitchPreference
if (switchPref.isChecked) {
FcmHelper.getFcmToken(requireContext())?.let {
pushManager.registerPusherWithFcmKey(it)
}
} else {
FcmHelper.getFcmToken(requireContext())?.let {
lifecycleScope.launch {
runCatching { pushManager.unregisterPusher(it) }
.fold(
{ session.refreshPushers() },
{
if (!isAdded) {
return@fold
}
// revert the check box
switchPref.isChecked = !switchPref.isChecked
Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show()
}
)
}
}
}
}
private fun updateEnabledForAccount(preference: Preference?) {
val pushRuleService = session
val switchPref = preference as SwitchPreference
@ -421,12 +407,9 @@ private fun Session.getEmailsWithPushInformation(): List<Pair<ThreePid.Email, Bo
}
private fun Session.getEmailsWithPushInformationLive(): LiveData<List<Pair<ThreePid.Email, Boolean>>> {
return getThreePidsLive(refreshData = false)
.distinctUntilChanged()
.map { threePids ->
val emailPushers = getPushers().filter { it.kind == Pusher.KIND_EMAIL }
threePids
.filterIsInstance<ThreePid.Email>()
.map { it to emailPushers.any { pusher -> pusher.pushKey == it.email } }
}
val emailThreePids = getThreePidsLive(refreshData = true).map { it.filterIsInstance<ThreePid.Email>() }
val emailPushers = getPushersLive().map { it.filter { pusher -> pusher.kind == Pusher.KIND_EMAIL } }
return combineLatest(emailThreePids, emailPushers) { emailThreePidsResult, emailPushersResult ->
emailThreePidsResult.map { it to emailPushersResult.any { pusher -> pusher.pushKey == it.email } }
}.distinctUntilChanged()
}