Merge pull request #7675 from vector-im/fix/mna/unified-push-selection

ANR when asking to select the notification method
This commit is contained in:
Maxime NATUREL 2022-12-05 16:58:09 +01:00 committed by GitHub
commit a2f8fed63c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1226 additions and 335 deletions

1
changelog.d/7653.bugfix Normal file
View file

@ -0,0 +1 @@
ANR when asking to select the notification method

View file

@ -17,7 +17,6 @@
package im.vector.app.push.fcm
import android.app.Activity
import android.content.Context
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.pushers.FcmHelper
@ -44,7 +43,7 @@ class FdroidFcmHelper @Inject constructor(
// No op
}
override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) {
// No op
}

View file

@ -15,7 +15,6 @@
*/
package im.vector.app.push.fcm
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.widget.Toast
@ -23,6 +22,7 @@ import androidx.core.content.edit
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.messaging.FirebaseMessaging
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.DefaultPreferences
@ -36,8 +36,8 @@ import javax.inject.Inject
* It has an alter ego in the fdroid variant.
*/
class GoogleFcmHelper @Inject constructor(
@DefaultPreferences
private val sharedPrefs: SharedPreferences,
@ApplicationContext private val context: Context,
@DefaultPreferences private val sharedPrefs: SharedPreferences,
) : FcmHelper {
companion object {
private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
@ -56,10 +56,9 @@ class GoogleFcmHelper @Inject constructor(
}
}
override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
// if (TextUtils.isEmpty(getFcmToken(activity))) {
override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) {
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(activity)) {
if (checkPlayServices(context)) {
try {
FirebaseMessaging.getInstance().token
.addOnSuccessListener { token ->
@ -75,7 +74,7 @@ class GoogleFcmHelper @Inject constructor(
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
}
} else {
Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show()
Toast.makeText(context, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show()
Timber.e("No valid Google Play Services found. Cannot use FCM.")
}
}

View file

@ -19,7 +19,7 @@ package im.vector.app.core.di
import android.content.Context
import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.core.session.ConfigureAndStartSessionUseCase
import im.vector.app.features.call.webrtc.WebRtcCallManager
@ -46,12 +46,12 @@ class ActiveSessionHolder @Inject constructor(
private val pushRuleTriggerListener: PushRuleTriggerListener,
private val sessionListener: SessionListener,
private val imageManager: ImageManager,
private val unifiedPushHelper: UnifiedPushHelper,
private val guardServiceStarter: GuardServiceStarter,
private val sessionInitializer: SessionInitializer,
private val applicationContext: Context,
private val authenticationService: AuthenticationService,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
) {
private var activeSessionReference: AtomicReference<Session?> = AtomicReference()
@ -85,7 +85,7 @@ class ActiveSessionHolder @Inject constructor(
incomingVerificationRequestHandler.stop()
pushRuleTriggerListener.stop()
// No need to unregister the pusher, the sign out will (should?) do it server side.
unifiedPushHelper.unregister(pushersManager = null)
unregisterUnifiedPushUseCase.execute(pushersManager = null)
guardServiceStarter.stop()
}

View file

@ -105,6 +105,7 @@ import im.vector.app.features.settings.ignored.IgnoredUsersViewModel
import im.vector.app.features.settings.labs.VectorSettingsLabsViewModel
import im.vector.app.features.settings.legals.LegalsViewModel
import im.vector.app.features.settings.locale.LocalePickerViewModel
import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceViewModel
import im.vector.app.features.settings.push.PushGatewaysViewModel
import im.vector.app.features.settings.threepids.ThreePidsSettingsViewModel
import im.vector.app.features.share.IncomingShareViewModel
@ -683,4 +684,11 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(AttachmentTypeSelectorViewModel::class)
fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(VectorSettingsNotificationPreferenceViewModel::class)
fun vectorSettingsNotificationPreferenceViewModelFactory(
factory: VectorSettingsNotificationPreferenceViewModel.Factory
): MavericksAssistedViewModelFactory<*, *>
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.pushers
import im.vector.app.core.di.ActiveSessionHolder
import javax.inject.Inject
class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
private val unifiedPushHelper: UnifiedPushHelper,
private val fcmHelper: FcmHelper,
private val activeSessionHolder: ActiveSessionHolder,
) {
fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
if (unifiedPushHelper.isEmbeddedDistributor()) {
fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
}
}
private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) {
val currentSession = activeSessionHolder.getActiveSession()
val currentPushers = currentSession.pushersService().getPushers()
currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
} else {
false
}
}

View file

@ -16,7 +16,6 @@
package im.vector.app.core.pushers
import android.app.Activity
import im.vector.app.core.di.ActiveSessionHolder
interface FcmHelper {
@ -39,11 +38,10 @@ interface FcmHelper {
/**
* onNewToken may not be called on application upgrade, so ensure my shared pref is set.
*
* @param activity the first launch Activity.
* @param pushersManager the instance to register the pusher on.
* @param registerPusher whether the pusher should be registered.
*/
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean)
fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean)
fun onEnterForeground(activeSessionHolder: ActiveSessionHolder)

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.pushers
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.features.VectorFeatures
import org.unifiedpush.android.connector.UnifiedPush
import javax.inject.Inject
class RegisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val vectorFeatures: VectorFeatures,
) {
sealed interface RegisterUnifiedPushResult {
object Success : RegisterUnifiedPushResult
object NeedToAskUserForDistributor : RegisterUnifiedPushResult
}
fun execute(distributor: String = ""): RegisterUnifiedPushResult {
if (distributor.isNotEmpty()) {
saveAndRegisterApp(distributor)
return RegisterUnifiedPushResult.Success
}
if (!vectorFeatures.allowExternalUnifiedPushDistributors()) {
saveAndRegisterApp(context.packageName)
return RegisterUnifiedPushResult.Success
}
if (UnifiedPush.getDistributor(context).isNotEmpty()) {
registerApp()
return RegisterUnifiedPushResult.Success
}
val distributors = UnifiedPush.getDistributors(context)
return if (distributors.size == 1) {
saveAndRegisterApp(distributors.first())
RegisterUnifiedPushResult.Success
} else {
RegisterUnifiedPushResult.NeedToAskUserForDistributor
}
}
private fun saveAndRegisterApp(distributor: String) {
UnifiedPush.saveDistributor(context, distributor)
registerApp()
}
private fun registerApp() {
UnifiedPush.registerApp(context)
}
}

View file

@ -17,18 +17,13 @@
package im.vector.app.core.pushers
import android.content.Context
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.annotation.MainThread
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.getApplicationLabel
import im.vector.app.features.VectorFeatures
import im.vector.app.features.settings.BackgroundSyncMode
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.util.MatrixJsonParser
@ -41,90 +36,14 @@ class UnifiedPushHelper @Inject constructor(
private val context: Context,
private val unifiedPushStore: UnifiedPushStore,
private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences,
private val matrix: Matrix,
private val vectorFeatures: VectorFeatures,
private val fcmHelper: FcmHelper,
) {
// Called when the home activity starts
// or when notifications are enabled
fun register(
activity: FragmentActivity,
onDoneRunnable: Runnable? = null,
) {
registerInternal(
activity,
onDoneRunnable = onDoneRunnable
)
}
// If registration is forced:
// * the current distributor (if any) is removed
// * The dialog is opened
//
// The registration is forced in 2 cases :
// * in the settings
// * in the troubleshoot list (doFix)
fun forceRegister(
activity: FragmentActivity,
pushersManager: PushersManager,
onDoneRunnable: Runnable? = null
) {
registerInternal(
activity,
force = true,
pushersManager = pushersManager,
onDoneRunnable = onDoneRunnable
)
}
private fun registerInternal(
activity: FragmentActivity,
force: Boolean = false,
pushersManager: PushersManager? = null,
onDoneRunnable: Runnable? = null
) {
activity.lifecycleScope.launch {
if (!vectorFeatures.allowExternalUnifiedPushDistributors()) {
UnifiedPush.saveDistributor(context, context.packageName)
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
return@launch
}
if (force) {
// Un-register first
unregister(pushersManager)
}
// the !force should not be needed
if (!force && UnifiedPush.getDistributor(context).isNotEmpty()) {
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
return@launch
}
val distributors = UnifiedPush.getDistributors(context)
if (!force && distributors.size == 1) {
UnifiedPush.saveDistributor(context, distributors.first())
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
} else {
openDistributorDialogInternal(
activity = activity,
onDoneRunnable = onDoneRunnable,
distributors = distributors
)
}
}
}
// There is no case where this function is called
// with a saved distributor and/or a pusher
private fun openDistributorDialogInternal(
activity: FragmentActivity,
onDoneRunnable: Runnable?,
distributors: List<String>
@MainThread
fun showSelectDistributorDialog(
context: Context,
onDistributorSelected: (String) -> Unit,
) {
val internalDistributorName = stringProvider.getString(
if (fcmHelper.isFirebaseAvailable()) {
@ -134,6 +53,7 @@ class UnifiedPushHelper @Inject constructor(
}
)
val distributors = UnifiedPush.getDistributors(context)
val distributorsName = distributors.map {
if (it == context.packageName) {
internalDistributorName
@ -142,44 +62,23 @@ class UnifiedPushHelper @Inject constructor(
}
}
MaterialAlertDialogBuilder(activity)
MaterialAlertDialogBuilder(context)
.setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title))
.setItems(distributorsName.toTypedArray()) { _, which ->
val distributor = distributors[which]
activity.lifecycleScope.launch {
UnifiedPush.saveDistributor(context, distributor)
Timber.i("Saving distributor: $distributor")
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
}
onDistributorSelected(distributor)
}
.setOnCancelListener {
// we do not want to change the distributor on behalf of the user
if (UnifiedPush.getDistributor(context).isEmpty()) {
// By default, use internal solution (fcm/background sync)
UnifiedPush.saveDistributor(context, context.packageName)
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
onDistributorSelected(context.packageName)
}
}
.setCancelable(true)
.show()
}
suspend fun unregister(pushersManager: PushersManager? = null) {
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
vectorPreferences.setFdroidSyncBackgroundMode(mode)
try {
getEndpointOrToken()?.let {
Timber.d("Removing $it")
pushersManager?.unregisterPusher(it)
}
} catch (e: Exception) {
Timber.d(e, "Probably unregistering a non existing pusher")
}
unifiedPushStore.storeUpEndpoint(null)
unifiedPushStore.storePushGateway(null)
UnifiedPush.unregisterApp(context)
}
@JsonClass(generateAdapter = true)
internal data class DiscoveryResponse(
@Json(name = "unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush()

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.pushers
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.features.settings.BackgroundSyncMode
import im.vector.app.features.settings.VectorPreferences
import org.unifiedpush.android.connector.UnifiedPush
import timber.log.Timber
import javax.inject.Inject
class UnregisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val vectorPreferences: VectorPreferences,
private val unifiedPushStore: UnifiedPushStore,
private val unifiedPushHelper: UnifiedPushHelper,
) {
suspend fun execute(pushersManager: PushersManager?) {
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
vectorPreferences.setFdroidSyncBackgroundMode(mode)
try {
unifiedPushHelper.getEndpointOrToken()?.let {
Timber.d("Removing $it")
pushersManager?.unregisterPusher(it)
}
} catch (e: Exception) {
Timber.d(e, "Probably unregistering a non existing pusher")
}
unifiedPushStore.storeUpEndpoint(null)
unifiedPushStore.storePushGateway(null)
UnifiedPush.unregisterApp(context)
}
}

View file

@ -44,8 +44,6 @@ import im.vector.app.core.extensions.restart
import im.vector.app.core.extensions.validateBackPressed
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.pushers.FcmHelper
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.startSharePlainTextIntent
@ -128,7 +126,6 @@ class HomeActivity :
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushersManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var shortcutsHandler: ShortcutsHandler
@ -137,7 +134,6 @@ class HomeActivity :
@Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter
@Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var nightlyProxy: NightlyProxy
@Inject lateinit var disclaimerDialog: DisclaimerDialog
@Inject lateinit var notificationPermissionManager: NotificationPermissionManager
@ -209,16 +205,6 @@ class HomeActivity :
isNewAppLayoutEnabled = vectorPreferences.isNewAppLayoutEnabled()
analyticsScreenName = MobileScreen.ScreenName.Home
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
unifiedPushHelper.register(this) {
if (unifiedPushHelper.isEmbeddedDistributor()) {
fcmHelper.ensureFcmTokenIsRetrieved(
this,
pushersManager,
homeActivityViewModel.shouldAddHttpPusher()
)
}
}
sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java]
roomListSharedActionViewModel = viewModelProvider[RoomListSharedActionViewModel::class.java]
views.drawerLayout.addDrawerListener(drawerListener)
@ -280,6 +266,7 @@ class HomeActivity :
HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes()
HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration()
is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession)
is HomeActivityViewEvents.AskUserForPushDistributor -> askUserToSelectPushDistributor()
}
}
homeActivityViewModel.onEach { renderState(it) }
@ -292,6 +279,12 @@ class HomeActivity :
homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
}
private fun askUserToSelectPushDistributor() {
unifiedPushHelper.showSelectDistributorDialog(this) { selection ->
homeActivityViewModel.handle(HomeActivityViewActions.RegisterPushDistributor(selection))
}
}
private fun handleShowNotificationDialog() {
notificationPermissionManager.eventuallyRequestPermission(this, postPermissionLauncher)
}
@ -415,14 +408,6 @@ class HomeActivity :
}
private fun renderState(state: HomeActivityViewState) {
lifecycleScope.launch {
if (state.areNotificationsSilenced) {
unifiedPushHelper.unregister(pushersManager)
} else {
unifiedPushHelper.register(this@HomeActivity)
}
}
when (val status = state.syncRequestState) {
is SyncRequestState.InitialSyncProgressing -> {
val initSyncStepStr = initSyncStepFormatter.format(status.initialSyncStep)

View file

@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed interface HomeActivityViewActions : VectorViewModelAction {
object ViewStarted : HomeActivityViewActions
object PushPromptHasBeenReviewed : HomeActivityViewActions
data class RegisterPushDistributor(val distributor: String) : HomeActivityViewActions
}

View file

@ -25,9 +25,11 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
val userItem: MatrixItem.UserItem,
val waitForIncomingRequest: Boolean = true,
) : HomeActivityViewEvents
data class CurrentSessionCannotBeVerified(
val userItem: MatrixItem.UserItem,
) : HomeActivityViewEvents
data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents
object PromptToEnableSessionPush : HomeActivityViewEvents
object ShowAnalyticsOptIn : HomeActivityViewEvents
@ -37,4 +39,5 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents
object StartRecoverySetupFlow : HomeActivityViewEvents
data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents
object AskUserForPushDistributor : HomeActivityViewEvents
}

View file

@ -16,7 +16,6 @@
package im.vector.app.features.home
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext
@ -27,7 +26,9 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.VectorFeatures
import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsType
@ -48,12 +49,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
@ -62,11 +61,9 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.pushrules.RuleIds
import org.matrix.android.sdk.api.session.room.model.Membership
@ -92,8 +89,10 @@ class HomeActivityViewModel @AssistedInject constructor(
private val analyticsTracker: AnalyticsTracker,
private val analyticsConfig: AnalyticsConfig,
private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore,
private val vectorFeatures: VectorFeatures,
private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase,
private val pushersManager: PushersManager,
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@AssistedFactory
@ -117,17 +116,36 @@ class HomeActivityViewModel @AssistedInject constructor(
private fun initialize() {
if (isInitialized) return
isInitialized = true
registerUnifiedPushIfNeeded()
cleanupFiles()
observeInitialSync()
checkSessionPushIsOn()
observeCrossSigningReset()
observeAnalytics()
observeReleaseNotes()
observeLocalNotificationsSilenced()
initThreadsMigration()
viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() }
}
private fun registerUnifiedPushIfNeeded() {
if (vectorPreferences.areNotificationEnabledForDevice()) {
registerUnifiedPush(distributor = "")
}
}
private fun registerUnifiedPush(distributor: String) {
viewModelScope.launch {
when (registerUnifiedPushUseCase.execute(distributor = distributor)) {
is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
_viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor)
}
RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
}
}
}
}
private fun observeReleaseNotes() = withState { state ->
if (vectorPreferences.isNewAppLayoutEnabled()) {
// we don't want to show release notes for new users or after relogin
@ -146,26 +164,6 @@ class HomeActivityViewModel @AssistedInject constructor(
}
}
fun shouldAddHttpPusher() = if (vectorPreferences.areNotificationEnabledForDevice()) {
val currentSession = activeSessionHolder.getActiveSession()
val currentPushers = currentSession.pushersService().getPushers()
currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
} else {
false
}
fun observeLocalNotificationsSilenced() {
val currentSession = activeSessionHolder.getActiveSession()
val deviceId = currentSession.cryptoService().getMyDevice().deviceId
viewModelScope.launch {
currentSession.accountDataService()
.getLiveUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
.asFlow()
.map { it.getOrNull()?.content?.toModel<LocalNotificationSettingsContent>()?.isSilenced ?: false }
.onEach { setState { copy(areNotificationsSilenced = it) } }
}
}
private fun observeAnalytics() {
if (analyticsConfig.isEnabled) {
analyticsStore.didAskUserConsentFlow
@ -501,6 +499,9 @@ class HomeActivityViewModel @AssistedInject constructor(
HomeActivityViewActions.ViewStarted -> {
initialize()
}
is HomeActivityViewActions.RegisterPushDistributor -> {
registerUnifiedPush(distributor = action.distributor)
}
}
}
}

View file

@ -23,5 +23,4 @@ import org.matrix.android.sdk.api.session.sync.SyncRequestState
data class HomeActivityViewState(
val syncRequestState: SyncRequestState = SyncRequestState.Idle,
val authenticationDescription: AuthenticationDescription? = null,
val areNotificationsSilenced: Boolean = false,
) : MavericksState

View file

@ -28,6 +28,8 @@ import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.toast
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.MobileScreen
@ -60,6 +62,19 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick
protected lateinit var session: Session
protected lateinit var errorFormatter: ErrorFormatter
/* ==========================================================================================
* ViewEvents
* ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.stream()
.onEach {
observer(it)
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
/* ==========================================================================================
* Views
* ========================================================================================== */
@ -148,7 +163,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick
}
}
protected fun displayErrorDialog(throwable: Throwable) {
protected fun displayErrorDialog(throwable: Throwable?) {
displayErrorDialog(errorFormatter.toHumanReadable(throwable))
}

View file

@ -18,17 +18,17 @@ package im.vector.app.features.settings.notifications
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
import javax.inject.Inject
class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val unifiedPushHelper: UnifiedPushHelper,
private val pushersManager: PushersManager,
private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
) {
suspend fun execute() {
@ -37,7 +37,7 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
togglePushNotificationUseCase.execute(deviceId, enabled = false)
} else {
unifiedPushHelper.unregister(pushersManager)
unregisterUnifiedPushUseCase.execute(pushersManager)
}
}
}

View file

@ -16,56 +16,44 @@
package im.vector.app.features.settings.notifications
import androidx.fragment.app.FragmentActivity
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.pushers.FcmHelper
import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val unifiedPushHelper: UnifiedPushHelper,
private val pushersManager: PushersManager,
private val fcmHelper: FcmHelper,
private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
) {
suspend fun execute(fragmentActivity: FragmentActivity) {
sealed interface EnableNotificationsResult {
object Success : EnableNotificationsResult
object Failure : EnableNotificationsResult
object NeedToAskUserForDistributor : EnableNotificationsResult
}
suspend fun execute(distributor: String = ""): EnableNotificationsResult {
val pusherForCurrentSession = pushersManager.getPusherForCurrentSession()
if (pusherForCurrentSession == null) {
registerPusher(fragmentActivity)
when (registerUnifiedPushUseCase.execute(distributor)) {
is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
return EnableNotificationsResult.NeedToAskUserForDistributor
}
RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = true)
}
}
}
val session = activeSessionHolder.getSafeActiveSession() ?: return
if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
val deviceId = session.sessionParams.deviceId ?: return
val session = activeSessionHolder.getSafeActiveSession() ?: return EnableNotificationsResult.Failure
val deviceId = session.sessionParams.deviceId ?: return EnableNotificationsResult.Failure
togglePushNotificationUseCase.execute(deviceId, enabled = true)
}
}
private suspend fun registerPusher(fragmentActivity: FragmentActivity) {
suspendCoroutine { continuation ->
try {
unifiedPushHelper.register(fragmentActivity) {
if (unifiedPushHelper.isEmbeddedDistributor()) {
fcmHelper.ensureFcmTokenIsRetrieved(
fragmentActivity,
pushersManager,
registerPusher = true
)
}
continuation.resume(Unit)
}
} catch (error: Exception) {
continuation.resumeWithException(error)
}
}
return EnableNotificationsResult.Success
}
}

View file

@ -22,6 +22,7 @@ import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
@ -29,6 +30,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.preference.Preference
import androidx.preference.SwitchPreference
import com.airbnb.mvrx.fragmentViewModel
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
@ -37,6 +39,7 @@ import im.vector.app.core.preference.VectorEditTextPreference
import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory
import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
import im.vector.app.core.pushers.FcmHelper
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper
@ -80,14 +83,15 @@ class VectorSettingsNotificationPreferenceFragment :
@Inject lateinit var guardServiceStarter: GuardServiceStarter
@Inject lateinit var vectorFeatures: VectorFeatures
@Inject lateinit var notificationPermissionManager: NotificationPermissionManager
@Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase
@Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase
@Inject lateinit var ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase
override var titleRes: Int = R.string.settings_notifications
override val preferenceXmlRes = R.xml.vector_settings_notifications
private var interactionListener: VectorSettingsFragmentInteractionListener? = null
private val viewModel: VectorSettingsNotificationPreferenceViewModel by fragmentViewModel()
private val notificationStartForActivityResult = registerStartForActivityResult { _ ->
// No op
}
@ -104,6 +108,23 @@ class VectorSettingsNotificationPreferenceFragment :
analyticsScreenName = MobileScreen.ScreenName.SettingsNotifications
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeViewEvents()
}
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled -> onNotificationsForDeviceEnabled()
VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled -> onNotificationsForDeviceDisabled()
is VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor -> askUserToSelectPushDistributor()
VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure -> displayErrorDialog(throwable = null)
VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged -> onNotificationMethodChanged()
}
}
}
override fun bindPref() {
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY)!!.let { pref ->
val pushRuleService = session.pushRuleService()
@ -121,23 +142,15 @@ class VectorSettingsNotificationPreferenceFragment :
}
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
?.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
if (isChecked) {
enableNotificationsForCurrentSessionUseCase.execute(requireActivity())
findPreference<VectorPreference>(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)
?.summary = unifiedPushHelper.getCurrentDistributorName()
notificationPermissionManager.eventuallyRequestPermission(
requireActivity(),
postPermissionLauncher,
showRationale = false,
ignorePreference = true
)
?.setOnPreferenceChangeListener { _, isChecked ->
val action = if (isChecked as Boolean) {
VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(pushDistributor = "")
} else {
disableNotificationsForCurrentSessionUseCase.execute()
notificationPermissionManager.eventuallyRevokePermission(requireActivity())
VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice
}
viewModel.handle(action)
// preference will be updated on ViewEvent reception
false
}
findPreference<VectorPreference>(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let {
@ -182,18 +195,7 @@ class VectorSettingsNotificationPreferenceFragment :
if (vectorFeatures.allowExternalUnifiedPushDistributors()) {
it.summary = unifiedPushHelper.getCurrentDistributorName()
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
unifiedPushHelper.forceRegister(requireActivity(), pushersManager) {
if (unifiedPushHelper.isEmbeddedDistributor()) {
fcmHelper.ensureFcmTokenIsRetrieved(
requireActivity(),
pushersManager,
vectorPreferences.areNotificationEnabledForDevice()
)
}
it.summary = unifiedPushHelper.getCurrentDistributorName()
session.pushersService().refreshPushers()
refreshBackgroundSyncPrefs()
}
askUserToSelectPushDistributor(withUnregister = true)
true
}
} else {
@ -207,6 +209,42 @@ class VectorSettingsNotificationPreferenceFragment :
handleSystemPreference()
}
private fun onNotificationsForDeviceEnabled() {
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
?.isChecked = true
findPreference<VectorPreference>(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)
?.summary = unifiedPushHelper.getCurrentDistributorName()
notificationPermissionManager.eventuallyRequestPermission(
requireActivity(),
postPermissionLauncher,
showRationale = false,
ignorePreference = true
)
}
private fun onNotificationsForDeviceDisabled() {
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
?.isChecked = false
notificationPermissionManager.eventuallyRevokePermission(requireActivity())
}
private fun askUserToSelectPushDistributor(withUnregister: Boolean = false) {
unifiedPushHelper.showSelectDistributorDialog(requireContext()) { selection ->
if (withUnregister) {
viewModel.handle(VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(selection))
} else {
viewModel.handle(VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(selection))
}
}
}
private fun onNotificationMethodChanged() {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)?.summary = unifiedPushHelper.getCurrentDistributorName()
session.pushersService().refreshPushers()
refreshBackgroundSyncPrefs()
}
private fun bindEmailNotifications() {
val initialEmails = session.getEmailsWithPushInformation()
bindEmailNotificationCategory(initialEmails)

View file

@ -0,0 +1,25 @@
/*
* 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.notifications
import im.vector.app.core.platform.VectorViewModelAction
sealed interface VectorSettingsNotificationPreferenceViewAction : VectorViewModelAction {
data class EnableNotificationsForDevice(val pushDistributor: String) : VectorSettingsNotificationPreferenceViewAction
object DisableNotificationsForDevice : VectorSettingsNotificationPreferenceViewAction
data class RegisterPushDistributor(val pushDistributor: String) : VectorSettingsNotificationPreferenceViewAction
}

View file

@ -0,0 +1,27 @@
/*
* 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.notifications
import im.vector.app.core.platform.VectorViewEvents
sealed interface VectorSettingsNotificationPreferenceViewEvent : VectorViewEvents {
object NotificationsForDeviceEnabled : VectorSettingsNotificationPreferenceViewEvent
object EnableNotificationForDeviceFailure : VectorSettingsNotificationPreferenceViewEvent
object NotificationsForDeviceDisabled : VectorSettingsNotificationPreferenceViewEvent
object AskUserForPushDistributor : VectorSettingsNotificationPreferenceViewEvent
object NotificationMethodChanged : VectorSettingsNotificationPreferenceViewEvent
}

View file

@ -0,0 +1,97 @@
/*
* 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.notifications
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorDummyViewState
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.launch
class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
@Assisted initialState: VectorDummyViewState,
private val pushersManager: PushersManager,
private val vectorPreferences: VectorPreferences,
private val enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase,
private val disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase,
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
) : VectorViewModel<VectorDummyViewState, VectorSettingsNotificationPreferenceViewAction, VectorSettingsNotificationPreferenceViewEvent>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<VectorSettingsNotificationPreferenceViewModel, VectorDummyViewState> {
override fun create(initialState: VectorDummyViewState): VectorSettingsNotificationPreferenceViewModel
}
companion object : MavericksViewModelFactory<VectorSettingsNotificationPreferenceViewModel, VectorDummyViewState> by hiltMavericksViewModelFactory()
override fun handle(action: VectorSettingsNotificationPreferenceViewAction) {
when (action) {
VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice -> handleDisableNotificationsForDevice()
is VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice -> handleEnableNotificationsForDevice(action.pushDistributor)
is VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor -> handleRegisterPushDistributor(action.pushDistributor)
}
}
private fun handleDisableNotificationsForDevice() {
viewModelScope.launch {
disableNotificationsForCurrentSessionUseCase.execute()
_viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled)
}
}
private fun handleEnableNotificationsForDevice(distributor: String) {
viewModelScope.launch {
when (enableNotificationsForCurrentSessionUseCase.execute(distributor)) {
EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure -> {
_viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure)
}
is EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor -> {
_viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor)
}
EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success -> {
_viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled)
}
}
}
}
private fun handleRegisterPushDistributor(distributor: String) {
viewModelScope.launch {
unregisterUnifiedPushUseCase.execute(pushersManager)
when (registerUnifiedPushUseCase.execute(distributor)) {
RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
_viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor)
}
RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
_viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged)
}
}
}
}
}

View file

@ -17,14 +17,17 @@
package im.vector.app.features.settings.troubleshoot
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
import androidx.work.WorkInfo
import androidx.work.WorkManager
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.pushers.PusherState
import javax.inject.Inject
@ -34,6 +37,8 @@ class TestEndpointAsTokenRegistration @Inject constructor(
private val pushersManager: PushersManager,
private val activeSessionHolder: ActiveSessionHolder,
private val unifiedPushHelper: UnifiedPushHelper,
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
) : TroubleshootTest(R.string.settings_troubleshoot_test_endpoint_registration_title) {
override fun perform(testParameters: TestParameters) {
@ -56,12 +61,34 @@ class TestEndpointAsTokenRegistration @Inject constructor(
)
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_endpoint_registration_quick_fix) {
override fun doFix() {
unifiedPushHelper.forceRegister(
context,
pushersManager
)
val workId = pushersManager.enqueueRegisterPusherWithFcmKey(endpoint)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo ->
unregisterThenRegister(testParameters, endpoint)
}
}
status = TestStatus.FAILED
} else {
description = stringProvider.getString(R.string.settings_troubleshoot_test_endpoint_registration_success)
status = TestStatus.SUCCESS
}
}
private fun unregisterThenRegister(testParameters: TestParameters, pushKey: String) {
activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch {
unregisterUnifiedPushUseCase.execute(pushersManager)
registerUnifiedPush(distributor = "", testParameters, pushKey)
}
}
private fun registerUnifiedPush(
distributor: String,
testParameters: TestParameters,
pushKey: String,
) {
when (registerUnifiedPushUseCase.execute(distributor)) {
is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor ->
askUserForDistributor(testParameters, pushKey)
RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
val workId = pushersManager.enqueueRegisterPusherWithFcmKey(pushKey)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context) { workInfo ->
if (workInfo != null) {
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
manager?.retry(testParameters)
@ -69,14 +96,17 @@ class TestEndpointAsTokenRegistration @Inject constructor(
manager?.retry(testParameters)
}
}
})
}
}
}
}
status = TestStatus.FAILED
} else {
description = stringProvider.getString(R.string.settings_troubleshoot_test_endpoint_registration_success)
status = TestStatus.SUCCESS
private fun askUserForDistributor(
testParameters: TestParameters,
pushKey: String,
) {
unifiedPushHelper.showSelectDistributorDialog(context) { selection ->
registerUnifiedPush(distributor = selection, testParameters, pushKey)
}
}
}

View file

@ -0,0 +1,105 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.pushers
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeFcmHelper
import im.vector.app.test.fakes.FakePushersManager
import im.vector.app.test.fakes.FakeUnifiedPushHelper
import im.vector.app.test.fixtures.PusherFixture
import org.junit.Test
class EnsureFcmTokenIsRetrievedUseCaseTest {
private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
private val fakeFcmHelper = FakeFcmHelper()
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val ensureFcmTokenIsRetrievedUseCase = EnsureFcmTokenIsRetrievedUseCase(
unifiedPushHelper = fakeUnifiedPushHelper.instance,
fcmHelper = fakeFcmHelper.instance,
activeSessionHolder = fakeActiveSessionHolder.instance,
)
@Test
fun `given no registered pusher and distributor as embedded when execute then ensure the FCM token is retrieved with register pusher option`() {
// Given
val aPushersManager = FakePushersManager()
fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true)
fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance)
val aSessionId = "aSessionId"
fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId)
val expectedPusher = PusherFixture.aPusher(deviceId = "")
fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher))
// When
ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true)
// Then
fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = true)
}
@Test
fun `given a registered pusher and distributor as embedded when execute then ensure the FCM token is retrieved without register pusher option`() {
// Given
val aPushersManager = FakePushersManager()
fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true)
fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance)
val aSessionId = "aSessionId"
fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId)
val expectedPusher = PusherFixture.aPusher(deviceId = aSessionId)
fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher))
// When
ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true)
// Then
fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false)
}
@Test
fun `given no registering asked and distributor as embedded when execute then ensure the FCM token is retrieved without register pusher option`() {
// Given
val aPushersManager = FakePushersManager()
fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true)
fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance)
val aSessionId = "aSessionId"
fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId)
val expectedPusher = PusherFixture.aPusher(deviceId = aSessionId)
fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher))
// When
ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = false)
// Then
fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false)
}
@Test
fun `given distributor as not embedded when execute then nothing is done`() {
// Given
val aPushersManager = FakePushersManager()
fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(false)
// When
ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true)
// Then
fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = true, inverse = true)
fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false, inverse = true)
}
}

View file

@ -0,0 +1,158 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.pushers
import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeVectorFeatures
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
import io.mockk.verifyAll
import io.mockk.verifyOrder
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.unifiedpush.android.connector.UnifiedPush
class RegisterUnifiedPushUseCaseTest {
private val fakeContext = FakeContext()
private val fakeVectorFeatures = FakeVectorFeatures()
private val registerUnifiedPushUseCase = RegisterUnifiedPushUseCase(
context = fakeContext.instance,
vectorFeatures = fakeVectorFeatures,
)
@Before
fun setup() {
mockkStatic(UnifiedPush::class)
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given non empty distributor when execute then distributor is saved and app is registered`() = runTest {
// Given
val aDistributor = "distributor"
justRun { UnifiedPush.registerApp(any()) }
justRun { UnifiedPush.saveDistributor(any(), any()) }
// When
val result = registerUnifiedPushUseCase.execute(aDistributor)
// Then
result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
verifyOrder {
UnifiedPush.saveDistributor(fakeContext.instance, aDistributor)
UnifiedPush.registerApp(fakeContext.instance)
}
}
@Test
fun `given external distributors are not allowed when execute then internal distributor is saved and app is registered`() = runTest {
// Given
val aPackageName = "packageName"
fakeContext.givenPackageName(aPackageName)
justRun { UnifiedPush.registerApp(any()) }
justRun { UnifiedPush.saveDistributor(any(), any()) }
fakeVectorFeatures.givenExternalDistributorsAreAllowed(false)
// When
val result = registerUnifiedPushUseCase.execute()
// Then
result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
verifyOrder {
UnifiedPush.saveDistributor(fakeContext.instance, aPackageName)
UnifiedPush.registerApp(fakeContext.instance)
}
}
@Test
fun `given a saved distributor and external distributors are allowed when execute then app is registered`() = runTest {
// Given
justRun { UnifiedPush.registerApp(any()) }
val aDistributor = "distributor"
every { UnifiedPush.getDistributor(any()) } returns aDistributor
fakeVectorFeatures.givenExternalDistributorsAreAllowed(true)
// When
val result = registerUnifiedPushUseCase.execute()
// Then
result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
verifyAll {
UnifiedPush.getDistributor(fakeContext.instance)
UnifiedPush.registerApp(fakeContext.instance)
}
}
@Test
fun `given no saved distributor and a unique distributor available when execute then the distributor is saved and app is registered`() = runTest {
// Given
justRun { UnifiedPush.registerApp(any()) }
justRun { UnifiedPush.saveDistributor(any(), any()) }
every { UnifiedPush.getDistributor(any()) } returns ""
fakeVectorFeatures.givenExternalDistributorsAreAllowed(true)
val aDistributor = "distributor"
every { UnifiedPush.getDistributors(any()) } returns listOf(aDistributor)
// When
val result = registerUnifiedPushUseCase.execute()
// Then
result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
verifyOrder {
UnifiedPush.getDistributor(fakeContext.instance)
UnifiedPush.getDistributors(fakeContext.instance)
UnifiedPush.saveDistributor(fakeContext.instance, aDistributor)
UnifiedPush.registerApp(fakeContext.instance)
}
}
@Test
fun `given no saved distributor and multiple distributors available when execute then result is to ask user`() = runTest {
// Given
every { UnifiedPush.getDistributor(any()) } returns ""
fakeVectorFeatures.givenExternalDistributorsAreAllowed(true)
val aDistributor1 = "distributor1"
val aDistributor2 = "distributor2"
every { UnifiedPush.getDistributors(any()) } returns listOf(aDistributor1, aDistributor2)
// When
val result = registerUnifiedPushUseCase.execute()
// Then
result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor
verifyOrder {
UnifiedPush.getDistributor(fakeContext.instance)
UnifiedPush.getDistributors(fakeContext.instance)
}
verify(inverse = true) {
UnifiedPush.saveDistributor(any(), any())
UnifiedPush.registerApp(any())
}
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.pushers
import im.vector.app.features.settings.BackgroundSyncMode
import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakePushersManager
import im.vector.app.test.fakes.FakeUnifiedPushHelper
import im.vector.app.test.fakes.FakeUnifiedPushStore
import im.vector.app.test.fakes.FakeVectorPreferences
import io.mockk.justRun
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verifyAll
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.unifiedpush.android.connector.UnifiedPush
class UnregisterUnifiedPushUseCaseTest {
private val fakeContext = FakeContext()
private val fakeVectorPreferences = FakeVectorPreferences()
private val fakeUnifiedPushStore = FakeUnifiedPushStore()
private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
private val unregisterUnifiedPushUseCase = UnregisterUnifiedPushUseCase(
context = fakeContext.instance,
vectorPreferences = fakeVectorPreferences.instance,
unifiedPushStore = fakeUnifiedPushStore.instance,
unifiedPushHelper = fakeUnifiedPushHelper.instance,
)
@Before
fun setup() {
mockkStatic(UnifiedPush::class)
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given pushersManager when execute then unregister and clean everything which is needed`() = runTest {
// Given
val aEndpoint = "endpoint"
fakeUnifiedPushHelper.givenGetEndpointOrTokenReturns(aEndpoint)
val aPushersManager = FakePushersManager()
aPushersManager.givenUnregisterPusher(aEndpoint)
justRun { UnifiedPush.unregisterApp(any()) }
fakeVectorPreferences.givenSetFdroidSyncBackgroundMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME)
fakeUnifiedPushStore.givenStorePushGateway(null)
fakeUnifiedPushStore.givenStoreUpEndpoint(null)
// When
unregisterUnifiedPushUseCase.execute(aPushersManager.instance)
// Then
fakeVectorPreferences.verifySetFdroidSyncBackgroundMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME)
aPushersManager.verifyUnregisterPusher(aEndpoint)
verifyAll {
UnifiedPush.unregisterApp(fakeContext.instance)
}
fakeUnifiedPushStore.verifyStorePushGateway(null)
fakeUnifiedPushStore.verifyStoreUpEndpoint(null)
}
}

View file

@ -16,11 +16,11 @@
package im.vector.app.features.settings.notifications
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakePushersManager
import im.vector.app.test.fakes.FakeUnifiedPushHelper
import io.mockk.coJustRun
import io.mockk.coVerify
import io.mockk.every
@ -33,17 +33,17 @@ private const val A_SESSION_ID = "session-id"
class DisableNotificationsForCurrentSessionUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
private val fakePushersManager = FakePushersManager()
private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
private val fakeTogglePushNotificationUseCase = mockk<TogglePushNotificationUseCase>()
private val fakeUnregisterUnifiedPushUseCase = mockk<UnregisterUnifiedPushUseCase>()
private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
unifiedPushHelper = fakeUnifiedPushHelper.instance,
pushersManager = fakePushersManager.instance,
checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
togglePushNotificationUseCase = fakeTogglePushNotificationUseCase,
unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase,
)
@Test
@ -67,12 +67,14 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
val fakeSession = fakeActiveSessionHolder.fakeSession
fakeSession.givenSessionId(A_SESSION_ID)
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
fakeUnifiedPushHelper.givenUnregister(fakePushersManager.instance)
coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
// When
disableNotificationsForCurrentSessionUseCase.execute()
// Then
fakeUnifiedPushHelper.verifyUnregister(fakePushersManager.instance)
coVerify {
fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
}
}
}

View file

@ -16,18 +16,18 @@
package im.vector.app.features.settings.notifications
import androidx.fragment.app.FragmentActivity
import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeFcmHelper
import im.vector.app.test.fakes.FakePushersManager
import im.vector.app.test.fakes.FakeUnifiedPushHelper
import io.mockk.coJustRun
import io.mockk.coVerify
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.junit.Test
private const val A_SESSION_ID = "session-id"
@ -35,53 +35,71 @@ private const val A_SESSION_ID = "session-id"
class EnableNotificationsForCurrentSessionUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
private val fakePushersManager = FakePushersManager()
private val fakeFcmHelper = FakeFcmHelper()
private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
private val fakeTogglePushNotificationUseCase = mockk<TogglePushNotificationUseCase>()
private val fakeRegisterUnifiedPushUseCase = mockk<RegisterUnifiedPushUseCase>()
private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk<EnsureFcmTokenIsRetrievedUseCase>()
private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
unifiedPushHelper = fakeUnifiedPushHelper.instance,
pushersManager = fakePushersManager.instance,
fcmHelper = fakeFcmHelper.instance,
checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
togglePushNotificationUseCase = fakeTogglePushNotificationUseCase,
registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase,
ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase,
)
@Test
fun `given no existing pusher for current session when execute then a new pusher is registered`() = runTest {
fun `given no existing pusher and a registered distributor when execute then a new pusher is registered and result is success`() = runTest {
// Given
val fragmentActivity = mockk<FragmentActivity>()
fakePushersManager.givenGetPusherForCurrentSessionReturns(null)
fakeUnifiedPushHelper.givenRegister(fragmentActivity)
fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true)
fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance)
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns false
// When
enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity)
// Then
fakeUnifiedPushHelper.verifyRegister(fragmentActivity)
fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance, registerPusher = true)
}
@Test
fun `given toggle via Pusher is possible when execute then current pusher is toggled to true`() = runTest {
// Given
val fragmentActivity = mockk<FragmentActivity>()
fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk())
val aDistributor = "distributor"
val fakeSession = fakeActiveSessionHolder.fakeSession
fakeSession.givenSessionId(A_SESSION_ID)
every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns true
fakePushersManager.givenGetPusherForCurrentSessionReturns(null)
every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) }
coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
// When
enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity)
val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor)
// Then
coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, true) }
result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success
verify {
fakeRegisterUnifiedPushUseCase.execute(aDistributor)
fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = true)
}
}
@Test
fun `given no existing pusher and a no registered distributor when execute then result is need to ask user for distributor`() = runTest {
// Given
val aDistributor = "distributor"
fakePushersManager.givenGetPusherForCurrentSessionReturns(null)
every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor
// When
val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor)
// Then
result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor
verify {
fakeRegisterUnifiedPushUseCase.execute(aDistributor)
}
}
@Test
fun `given no deviceId for current session when execute then result is failure`() = runTest {
// Given
val aDistributor = "distributor"
val fakeSession = fakeActiveSessionHolder.fakeSession
fakeSession.givenSessionId(null)
fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk())
every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor
// When
val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor)
// Then
result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure
}
}

View file

@ -0,0 +1,202 @@
/*
* 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.notifications
import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.core.platform.VectorDummyViewState
import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.test.fakes.FakePushersManager
import im.vector.app.test.fakes.FakeVectorPreferences
import im.vector.app.test.test
import im.vector.app.test.testDispatcher
import io.mockk.coEvery
import io.mockk.coJustRun
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.justRun
import io.mockk.mockk
import org.junit.Rule
import org.junit.Test
class VectorSettingsNotificationPreferenceViewModelTest {
@get:Rule
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
private val fakePushersManager = FakePushersManager()
private val fakeVectorPreferences = FakeVectorPreferences()
private val fakeEnableNotificationsForCurrentSessionUseCase = mockk<EnableNotificationsForCurrentSessionUseCase>()
private val fakeDisableNotificationsForCurrentSessionUseCase = mockk<DisableNotificationsForCurrentSessionUseCase>()
private val fakeUnregisterUnifiedPushUseCase = mockk<UnregisterUnifiedPushUseCase>()
private val fakeRegisterUnifiedPushUseCase = mockk<RegisterUnifiedPushUseCase>()
private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk<EnsureFcmTokenIsRetrievedUseCase>()
private fun createViewModel() = VectorSettingsNotificationPreferenceViewModel(
initialState = VectorDummyViewState(),
pushersManager = fakePushersManager.instance,
vectorPreferences = fakeVectorPreferences.instance,
enableNotificationsForCurrentSessionUseCase = fakeEnableNotificationsForCurrentSessionUseCase,
disableNotificationsForCurrentSessionUseCase = fakeDisableNotificationsForCurrentSessionUseCase,
unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase,
registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase,
ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase,
)
@Test
fun `given DisableNotificationsForDevice action when handling action then disable use case is called`() {
// Given
val viewModel = createViewModel()
val action = VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice
coJustRun { fakeDisableNotificationsForCurrentSessionUseCase.execute() }
val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest
.assertEvent { event -> event == expectedEvent }
.finish()
coVerify {
fakeDisableNotificationsForCurrentSessionUseCase.execute()
}
}
@Test
fun `given EnableNotificationsForDevice action and enable success when handling action then enable use case is called`() {
// Given
val viewModel = createViewModel()
val aDistributor = "aDistributor"
val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor)
coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns
EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success
val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest
.assertEvent { event -> event == expectedEvent }
.finish()
coVerify {
fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor)
}
}
@Test
fun `given EnableNotificationsForDevice action and enable needs user choice when handling action then enable use case is called`() {
// Given
val viewModel = createViewModel()
val aDistributor = "aDistributor"
val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor)
coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns
EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor
val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest
.assertEvent { event -> event == expectedEvent }
.finish()
coVerify {
fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor)
}
}
@Test
fun `given EnableNotificationsForDevice action and enable failure when handling action then enable use case is called`() {
// Given
val viewModel = createViewModel()
val aDistributor = "aDistributor"
val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor)
coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns
EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure
val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest
.assertEvent { event -> event == expectedEvent }
.finish()
coVerify {
fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor)
}
}
@Test
fun `given RegisterPushDistributor action and register success when handling action then register use case is called`() {
// Given
val viewModel = createViewModel()
val aDistributor = "aDistributor"
val action = VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(aDistributor)
coEvery { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
val areNotificationsEnabled = true
fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled)
justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) }
val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest
.assertEvent { event -> event == expectedEvent }
.finish()
coVerifyOrder {
fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
fakeRegisterUnifiedPushUseCase.execute(aDistributor)
fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = areNotificationsEnabled)
}
}
@Test
fun `given RegisterPushDistributor action and register needs user choice when handling action then register use case is called`() {
// Given
val viewModel = createViewModel()
val aDistributor = "aDistributor"
val action = VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(aDistributor)
coEvery { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor
coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor
// When
val viewModelTest = viewModel.test()
viewModel.handle(action)
// Then
viewModelTest
.assertEvent { event -> event == expectedEvent }
.finish()
coVerifyOrder {
fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
fakeRegisterUnifiedPushUseCase.execute(aDistributor)
}
}
}

View file

@ -81,4 +81,8 @@ class FakeContext(
givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance)
return fakeClipboardManager
}
fun givenPackageName(name: String) {
every { instance.packageName } returns name
}
}

View file

@ -16,7 +16,6 @@
package im.vector.app.test.fakes
import androidx.fragment.app.FragmentActivity
import im.vector.app.core.pushers.FcmHelper
import im.vector.app.core.pushers.PushersManager
import io.mockk.justRun
@ -27,18 +26,15 @@ class FakeFcmHelper {
val instance = mockk<FcmHelper>()
fun givenEnsureFcmTokenIsRetrieved(
fragmentActivity: FragmentActivity,
pushersManager: PushersManager,
) {
justRun { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, any()) }
fun givenEnsureFcmTokenIsRetrieved(pushersManager: PushersManager) {
justRun { instance.ensureFcmTokenIsRetrieved(pushersManager, any()) }
}
fun verifyEnsureFcmTokenIsRetrieved(
fragmentActivity: FragmentActivity,
pushersManager: PushersManager,
registerPusher: Boolean,
inverse: Boolean = false,
) {
verify { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, registerPusher) }
verify(inverse = inverse) { instance.ensureFcmTokenIsRetrieved(pushersManager, registerPusher) }
}
}

View file

@ -17,6 +17,8 @@
package im.vector.app.test.fakes
import im.vector.app.core.pushers.PushersManager
import io.mockk.coJustRun
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import org.matrix.android.sdk.api.session.pushers.Pusher
@ -28,4 +30,12 @@ class FakePushersManager {
fun givenGetPusherForCurrentSessionReturns(pusher: Pusher?) {
every { instance.getPusherForCurrentSession() } returns pusher
}
fun givenUnregisterPusher(pushKey: String) {
coJustRun { instance.unregisterPusher(pushKey) }
}
fun verifyUnregisterPusher(pushKey: String) {
coVerify { instance.unregisterPusher(pushKey) }
}
}

View file

@ -16,38 +16,19 @@
package im.vector.app.test.fakes
import androidx.fragment.app.FragmentActivity
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper
import io.mockk.coJustRun
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
class FakeUnifiedPushHelper {
val instance = mockk<UnifiedPushHelper>()
fun givenRegister(fragmentActivity: FragmentActivity) {
every { instance.register(fragmentActivity, any()) } answers {
secondArg<Runnable>().run()
}
}
fun verifyRegister(fragmentActivity: FragmentActivity) {
verify { instance.register(fragmentActivity, any()) }
}
fun givenUnregister(pushersManager: PushersManager) {
coJustRun { instance.unregister(pushersManager) }
}
fun verifyUnregister(pushersManager: PushersManager) {
coVerify { instance.unregister(pushersManager) }
}
fun givenIsEmbeddedDistributorReturns(isEmbedded: Boolean) {
every { instance.isEmbeddedDistributor() } returns isEmbedded
}
fun givenGetEndpointOrTokenReturns(endpoint: String?) {
every { instance.getEndpointOrToken() } returns endpoint
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import im.vector.app.core.pushers.UnifiedPushStore
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.verify
class FakeUnifiedPushStore {
val instance = mockk<UnifiedPushStore>()
fun givenStoreUpEndpoint(endpoint: String?) {
justRun { instance.storeUpEndpoint(endpoint) }
}
fun verifyStoreUpEndpoint(endpoint: String?) {
verify { instance.storeUpEndpoint(endpoint) }
}
fun givenStorePushGateway(gateway: String?) {
justRun { instance.storePushGateway(gateway) }
}
fun verifyStorePushGateway(gateway: String?) {
verify { instance.storePushGateway(gateway) }
}
}

View file

@ -54,4 +54,8 @@ class FakeVectorFeatures : VectorFeatures by spyk<DefaultVectorFeatures>() {
fun givenUnverifiedSessionsAlertEnabled(isEnabled: Boolean) {
every { isUnverifiedSessionsAlertEnabled() } returns isEnabled
}
fun givenExternalDistributorsAreAllowed(allowed: Boolean) {
every { allowExternalUnifiedPushDistributors() } returns allowed
}
}

View file

@ -16,6 +16,7 @@
package im.vector.app.test.fakes
import im.vector.app.features.settings.BackgroundSyncMode
import im.vector.app.features.settings.VectorPreferences
import io.mockk.every
import io.mockk.justRun
@ -60,4 +61,16 @@ class FakeVectorPreferences {
fun givenUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) {
every { instance.getUnverifiedSessionsAlertLastShownMillis(any()) } returns lastShownMillis
}
fun givenSetFdroidSyncBackgroundMode(mode: BackgroundSyncMode) {
justRun { instance.setFdroidSyncBackgroundMode(mode) }
}
fun verifySetFdroidSyncBackgroundMode(mode: BackgroundSyncMode) {
verify { instance.setFdroidSyncBackgroundMode(mode) }
}
fun givenAreNotificationsEnabledForDevice(notificationsEnabled: Boolean) {
every { instance.areNotificationEnabledForDevice() } returns notificationsEnabled
}
}