From 3e772cd406e88c0aa09b13fec20785a18fc84b60 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Mon, 20 Nov 2023 17:24:11 +0100 Subject: [PATCH] Sync Bookmarks limit, handle and surface errors --- .../sync/CredentialsSyncDataPersister.kt | 4 +- .../sync/CredentialsSyncDataPersisterTest.kt | 14 +- saved-sites/saved-sites-impl/build.gradle | 1 + .../impl/sync/RealSavedSitesSyncStore.kt | 37 +++++- .../impl/sync/SaveSiteRateLimitView.kt | 121 ++++++++++++++++++ .../SaveSitesRateLimitSyncMessagePlugin.kt | 31 +++++ .../impl/sync/SavedSiteRateLimitViewModel.kt | 83 ++++++++++++ .../sync/SavedSitesSyncFeatureListener.kt | 77 +++++++++++ .../sync/SavedSitesSyncNotificationBuilder.kt | 58 +++++++++ .../impl/sync/SavedSitesSyncPersister.kt | 10 +- .../res/layout/notification_rate_limit.xml | 32 +++++ .../view_save_site_rate_limit_warning.xml | 30 +++++ .../src/main/res/values/donottranslate.xml | 2 + .../duckduckgo/sync/api/SyncMessagePlugin.kt | 24 ++++ .../sync/api/SyncNotificationChannel.kt | 19 +++ .../com/duckduckgo/sync/api/engine/Models.kt | 3 +- .../sync/api/engine/SyncableDataPersister.kt | 2 +- .../sync/impl/SyncAccountRepository.kt | 11 +- .../impl/SyncNotificationChannelPlugin.kt | 42 ++++++ .../com/duckduckgo/sync/impl/SyncService.kt | 2 - .../sync/impl/di/SyncMessageModule.kt | 32 +++++ .../sync/impl/engine/RealSyncEngine.kt | 16 ++- .../duckduckgo/sync/impl/ui/SyncActivity.kt | 18 +++ .../src/main/res/layout/view_sync_enabled.xml | 6 + .../src/main/res/values/donottranslate.xml | 1 + .../impl/engine/FakeSyncableDataPersister.kt | 2 +- .../impl/SettingsSyncDataPersister.kt | 4 +- .../impl/SettingsSyncDataPersisterTest.kt | 12 +- 28 files changed, 650 insertions(+), 44 deletions(-) create mode 100644 saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SaveSiteRateLimitView.kt create mode 100644 saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SaveSitesRateLimitSyncMessagePlugin.kt create mode 100644 saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSiteRateLimitViewModel.kt create mode 100644 saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncFeatureListener.kt create mode 100644 saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncNotificationBuilder.kt create mode 100644 saved-sites/saved-sites-impl/src/main/res/layout/notification_rate_limit.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/layout/view_save_site_rate_limit_warning.xml create mode 100644 sync/sync-api/src/main/java/com/duckduckgo/sync/api/SyncMessagePlugin.kt create mode 100644 sync/sync-api/src/main/java/com/duckduckgo/sync/api/SyncNotificationChannel.kt create mode 100644 sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncNotificationChannelPlugin.kt create mode 100644 sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/di/SyncMessageModule.kt diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/sync/CredentialsSyncDataPersister.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/sync/CredentialsSyncDataPersister.kt index ff68c6af7a2c..218272c0b723 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/sync/CredentialsSyncDataPersister.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/sync/CredentialsSyncDataPersister.kt @@ -45,7 +45,7 @@ class CredentialsSyncDataPersister @Inject constructor( private val strategies: DaggerMap, private val appBuildConfig: AppBuildConfig, ) : SyncableDataPersister { - override fun persist( + override fun onSuccess( changes: SyncChangesResponse, conflictResolution: SyncConflictResolution, ): SyncMergeResult { @@ -63,7 +63,7 @@ class CredentialsSyncDataPersister @Inject constructor( } override fun onError(error: SyncErrorResponse) { - TODO("Not yet implemented") + // TODO: implement } private fun process( diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsSyncDataPersisterTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsSyncDataPersisterTest.kt index c4f6945f3abe..d36fb88d8fff 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsSyncDataPersisterTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/sync/CredentialsSyncDataPersisterTest.kt @@ -82,7 +82,7 @@ internal class CredentialsSyncDataPersisterTest { fun whenValidatingCorruptedDataThenResultIsError() { val updatesJSON = FileUtilities.loadText(javaClass.classLoader!!, "json/sync/merger_invalid_data.json") val corruptedChanges = SyncChangesResponse(CREDENTIALS, updatesJSON) - val result = syncPersister.persist(corruptedChanges, TIMESTAMP) + val result = syncPersister.onSuccess(corruptedChanges, TIMESTAMP) assertTrue(result is Error) } @@ -91,7 +91,7 @@ internal class CredentialsSyncDataPersisterTest { fun whenValidatingNullEntriesThenResultIsError() { val updatesJSON = FileUtilities.loadText(javaClass.classLoader!!, "json/sync/merger_null_entries.json") val corruptedChanges = SyncChangesResponse(CREDENTIALS, updatesJSON) - val result = syncPersister.persist(corruptedChanges, TIMESTAMP) + val result = syncPersister.onSuccess(corruptedChanges, TIMESTAMP) assertTrue(result is Error) } @@ -100,7 +100,7 @@ internal class CredentialsSyncDataPersisterTest { fun whenProcessingDataInEmptyDBThenResultIsSuccess() { val updatesJSON = FileUtilities.loadText(javaClass.classLoader!!, "json/sync/merger_first_get.json") val validChanges = SyncChangesResponse(CREDENTIALS, updatesJSON) - val result = syncPersister.persist(validChanges, DEDUPLICATION) + val result = syncPersister.onSuccess(validChanges, DEDUPLICATION) assertTrue(result is Success) } @@ -109,7 +109,7 @@ internal class CredentialsSyncDataPersisterTest { fun whenMergingEmptyEntriesThenResultIsSuccess() { val updatesJSON = FileUtilities.loadText(javaClass.classLoader!!, "json/sync/merger_empty_entries.json") val corruptedChanges = SyncChangesResponse(CREDENTIALS, updatesJSON) - val result = syncPersister.persist(corruptedChanges, TIMESTAMP) + val result = syncPersister.onSuccess(corruptedChanges, TIMESTAMP) assertTrue(result is Success) } @@ -118,14 +118,14 @@ internal class CredentialsSyncDataPersisterTest { fun whenMergingWithDeletedDataThenResultIsSuccess() { val updatesJSON = FileUtilities.loadText(javaClass.classLoader!!, "json/sync/merger_deleted_entries.json") val deletedChanges = SyncChangesResponse(CREDENTIALS, updatesJSON) - val result = syncPersister.persist(deletedChanges, TIMESTAMP) + val result = syncPersister.onSuccess(deletedChanges, TIMESTAMP) assertTrue(result is Success) } @Test fun whenPersistWithAnotherTypeThenReturnFalse() { - val result = syncPersister.persist( + val result = syncPersister.onSuccess( SyncChangesResponse(BOOKMARKS, ""), DEDUPLICATION, ) @@ -140,7 +140,7 @@ internal class CredentialsSyncDataPersisterTest { val updatesJSON = FileUtilities.loadText(javaClass.classLoader!!, "json/sync/merger_first_get.json") val validChanges = SyncChangesResponse(CREDENTIALS, updatesJSON) - val result = syncPersister.persist(validChanges, DEDUPLICATION) + val result = syncPersister.onSuccess(validChanges, DEDUPLICATION) assertTrue(result is Success) assertNull(dao.getSyncMetadata(1L)) diff --git a/saved-sites/saved-sites-impl/build.gradle b/saved-sites/saved-sites-impl/build.gradle index e86b3778acf6..d35e345fb7f9 100644 --- a/saved-sites/saved-sites-impl/build.gradle +++ b/saved-sites/saved-sites-impl/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation project(path: ':sync-api') implementation project(path: ':sync-settings-api') implementation project(path: ':browser-api') + implementation project(path: ':navigation-api') api project(path: ':saved-sites-store') anvil project(path: ':anvil-compiler') diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/RealSavedSitesSyncStore.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/RealSavedSitesSyncStore.kt index 537011516c96..febd7a87387e 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/RealSavedSitesSyncStore.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/RealSavedSitesSyncStore.kt @@ -19,22 +19,40 @@ package com.duckduckgo.savedsites.impl.sync import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import dagger.SingleInstanceIn import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch interface SavedSitesSyncStore { var serverModifiedSince: String var startTimeStamp: String var clientModifiedSince: String - var limitExceeded: Boolean + var isSyncPaused: Boolean + fun isSyncPausedFlow(): Flow } @ContributesBinding(AppScope::class) @SingleInstanceIn(AppScope::class) -class RealSavedSitesSyncStore @Inject constructor(private val context: Context) : SavedSitesSyncStore { +class RealSavedSitesSyncStore @Inject constructor( + private val context: Context, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, +) : SavedSitesSyncStore { + private val syncPausedSharedFlow = MutableSharedFlow(replay = 1, onBufferOverflow = DROP_OLDEST) + init { + appCoroutineScope.launch(dispatcherProvider.io()) { + syncPausedSharedFlow.emit(isSyncPaused) + } + } override var serverModifiedSince: String get() = preferences.getString(KEY_SERVER_MODIFIED_SINCE, "0") ?: "0" set(value) = preferences.edit(true) { putString(KEY_SERVER_MODIFIED_SINCE, value) } @@ -44,9 +62,20 @@ class RealSavedSitesSyncStore @Inject constructor(private val context: Context) override var clientModifiedSince: String get() = preferences.getString(KEY_CLIENT_MODIFIED_SINCE, "0") ?: "0" set(value) = preferences.edit(true) { putString(KEY_CLIENT_MODIFIED_SINCE, value) } - override var limitExceeded: Boolean + override var isSyncPaused: Boolean get() = preferences.getBoolean(KEY_CLIENT_LIMIT_EXCEEDED, false) ?: false - set(value) = preferences.edit(true) { putBoolean(KEY_CLIENT_LIMIT_EXCEEDED, value) } + set(value) { + preferences.edit(true) { putBoolean(KEY_CLIENT_LIMIT_EXCEEDED, value) } + emitNewValue() + } + + override fun isSyncPausedFlow(): Flow = syncPausedSharedFlow + + private fun emitNewValue() { + appCoroutineScope.launch(dispatcherProvider.io()) { + syncPausedSharedFlow.emit(isSyncPaused) + } + } private val preferences: SharedPreferences get() = context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SaveSiteRateLimitView.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SaveSiteRateLimitView.kt new file mode 100644 index 000000000000..0ac156927476 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SaveSiteRateLimitView.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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 com.duckduckgo.savedsites.impl.sync + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import androidx.core.view.isVisible +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewTreeLifecycleOwner +import androidx.lifecycle.findViewTreeViewModelStoreOwner +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.browser.api.ui.BrowserScreens.BookmarksScreenNoParams +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.common.utils.ConflatedJob +import com.duckduckgo.di.scopes.ViewScope +import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.saved.sites.impl.R +import com.duckduckgo.saved.sites.impl.databinding.ViewSaveSiteRateLimitWarningBinding +import com.duckduckgo.savedsites.impl.sync.SavedSiteRateLimitViewModel.Command +import com.duckduckgo.savedsites.impl.sync.SavedSiteRateLimitViewModel.Command.NavigateToBookmarks +import com.duckduckgo.savedsites.impl.sync.SavedSiteRateLimitViewModel.ViewState +import dagger.android.support.AndroidSupportInjection +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@InjectWith(ViewScope::class) +class SaveSiteRateLimitView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : FrameLayout(context, attrs, defStyle) { + + @Inject + lateinit var globalActivityStarter: GlobalActivityStarter + + @Inject + lateinit var viewModelFactory: SavedSiteRateLimitViewModel.Factory + + private var coroutineScope: CoroutineScope? = null + + private var job: ConflatedJob = ConflatedJob() + + private val binding: ViewSaveSiteRateLimitWarningBinding by viewBinding() + + private val viewModel: SavedSiteRateLimitViewModel by lazy { + ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[SavedSiteRateLimitViewModel::class.java] + } + + override fun onAttachedToWindow() { + AndroidSupportInjection.inject(this) + super.onAttachedToWindow() + + ViewTreeLifecycleOwner.get(this)?.lifecycle?.addObserver(viewModel) + + configureViewListeners() + + @SuppressLint("NoHardcodedCoroutineDispatcher") + coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + viewModel.viewState() + .onEach { render(it) } + .launchIn(coroutineScope!!) + + job += viewModel.commands() + .onEach { processCommands(it) } + .launchIn(coroutineScope!!) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + ViewTreeLifecycleOwner.get(this)?.lifecycle?.removeObserver(viewModel) + coroutineScope?.cancel() + job.cancel() + coroutineScope = null + } + + private fun processCommands(command: Command) { + when (command) { + NavigateToBookmarks -> navigateToBookmarks() + } + } + + private fun render(viewState: ViewState) { + this.isVisible = viewState.warningVisible + } + + private fun configureViewListeners() { + binding.saveSiteRateLimitWarning.setClickableLink( + "manage_bookmarks", + context.getText(R.string.saved_site_limit_warning), + onClick = { + viewModel.onWarningActionClicked() + }, + ) + } + + private fun navigateToBookmarks() { + globalActivityStarter.start(this.context, BookmarksScreenNoParams) + } +} diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SaveSitesRateLimitSyncMessagePlugin.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SaveSitesRateLimitSyncMessagePlugin.kt new file mode 100644 index 000000000000..226489f0476e --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SaveSitesRateLimitSyncMessagePlugin.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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 com.duckduckgo.savedsites.impl.sync + +import android.content.Context +import android.view.View +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.sync.api.SyncMessagePlugin +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject + +@ContributesMultibinding(scope = ActivityScope::class) +class SaveSitesRateLimitSyncMessagePlugin @Inject constructor() : SyncMessagePlugin { + override fun getView(context: Context): View { + return SaveSiteRateLimitView(context) + } +} diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSiteRateLimitViewModel.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSiteRateLimitViewModel.kt new file mode 100644 index 000000000000..3d1711b08c13 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSiteRateLimitViewModel.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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 com.duckduckgo.savedsites.impl.sync + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.savedsites.impl.sync.DisplayModeViewModel.ViewState +import javax.inject.Inject +import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class SavedSiteRateLimitViewModel( + private val savedSitesSyncStore: SavedSitesSyncStore, + private val dispatcherProvider: DispatcherProvider, +) : ViewModel(), DefaultLifecycleObserver { + + data class ViewState( + val warningVisible: Boolean = false, + ) + + sealed class Command { + data object NavigateToBookmarks : Command() + } + + private val command = Channel(1, DROP_OLDEST) + + fun viewState(): Flow = savedSitesSyncStore.isSyncPausedFlow() + .map { syncPaused -> + ViewState( + warningVisible = syncPaused, + ) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ViewState()) + + fun commands(): Flow = command.receiveAsFlow() + + fun onWarningActionClicked() { + viewModelScope.launch { + command.send(Command.NavigateToBookmarks) + } + } + + @Suppress("UNCHECKED_CAST") + class Factory @Inject constructor( + private val savedSitesSyncStore: SavedSitesSyncStore, + private val dispatcherProvider: DispatcherProvider, + ) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T { + return with(modelClass) { + when { + isAssignableFrom(SavedSiteRateLimitViewModel::class.java) -> SavedSiteRateLimitViewModel( + savedSitesSyncStore, + dispatcherProvider, + ) + else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") + } + } as T + } + } +} diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncFeatureListener.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncFeatureListener.kt new file mode 100644 index 000000000000..b1cfab8fa420 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncFeatureListener.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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 com.duckduckgo.savedsites.impl.sync + +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.sync.api.engine.FeatureSyncError +import com.duckduckgo.sync.api.engine.FeatureSyncError.COLLECTION_LIMIT_REACHED +import com.duckduckgo.sync.api.engine.SyncChangesResponse +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject + +interface SavedSitesSyncFeatureListener { + fun onSuccess(changes: SyncChangesResponse) + fun onError(syncError: FeatureSyncError) + fun onSyncDisabled() +} + +@ContributesBinding(AppScope::class) +class AppSavedSitesSyncFeatureListener @Inject constructor( + private val context: Context, + private val savedSitesSyncStore: SavedSitesSyncStore, + private val notificationManager: NotificationManagerCompat, + private val notificationBuilder: SavedSitesSyncNotificationBuilder, +) : SavedSitesSyncFeatureListener { + + override fun onSuccess(changes: SyncChangesResponse) { + if (changes.jsonString.isEmpty()) return // no changes, skip + + if (savedSitesSyncStore.isSyncPaused) { + savedSitesSyncStore.isSyncPaused = false + cancelNotification() + } + } + + override fun onError(syncError: FeatureSyncError) { + when (syncError) { + COLLECTION_LIMIT_REACHED -> { + if (!savedSitesSyncStore.isSyncPaused) { + triggerNotification() + } + savedSitesSyncStore.isSyncPaused = true + } + } + } + + override fun onSyncDisabled() { + savedSitesSyncStore.isSyncPaused = false + cancelNotification() + } + + private fun triggerNotification() { + notificationManager.notify( + 666, + notificationBuilder.buildRateLimitNotification(context), + ) + } + + private fun cancelNotification() { + notificationManager.cancel(666) + } +} diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncNotificationBuilder.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncNotificationBuilder.kt new file mode 100644 index 000000000000..4efee8f61d3b --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncNotificationBuilder.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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 com.duckduckgo.savedsites.impl.sync + +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.widget.RemoteViews +import androidx.core.app.NotificationCompat +import androidx.core.app.TaskStackBuilder +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.saved.sites.impl.R.layout +import com.duckduckgo.sync.api.SYNC_NOTIFICATION_CHANNEL_ID +import com.duckduckgo.sync.api.SyncActivityWithEmptyParams +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject + +interface SavedSitesSyncNotificationBuilder { + fun buildRateLimitNotification(context: Context): Notification +} + +@ContributesBinding(AppScope::class) +class AppSavedSitesSyncNotificationBuilder @Inject constructor( + private val globalGlobalActivityStarter: GlobalActivityStarter, +) : SavedSitesSyncNotificationBuilder { + override fun buildRateLimitNotification(context: Context): Notification { + return NotificationCompat.Builder(context, SYNC_NOTIFICATION_CHANNEL_ID) + .setSmallIcon(com.duckduckgo.mobile.android.R.drawable.notification_logo) + .setStyle(NotificationCompat.DecoratedCustomViewStyle()) + .setContentIntent(getPendingIntent(context)) + .setCustomContentView(RemoteViews(context.packageName, layout.notification_rate_limit)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .build() + } + + private fun getPendingIntent(context: Context): PendingIntent? = TaskStackBuilder.create(context).run { + addNextIntentWithParentStack( + globalGlobalActivityStarter.startIntent(context, SyncActivityWithEmptyParams)!!, + ) + getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } +} diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncPersister.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncPersister.kt index b539c27e2103..e8651c944357 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncPersister.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncPersister.kt @@ -22,7 +22,6 @@ import com.duckduckgo.savedsites.api.SavedSitesRepository import com.duckduckgo.savedsites.impl.sync.algorithm.SavedSitesSyncPersisterAlgorithm import com.duckduckgo.sync.api.engine.SyncChangesResponse import com.duckduckgo.sync.api.engine.SyncDataValidationResult -import com.duckduckgo.sync.api.engine.FeatureSyncError.COLLECTION_LIMIT_REACHED import com.duckduckgo.sync.api.engine.SyncErrorResponse import com.duckduckgo.sync.api.engine.SyncMergeResult import com.duckduckgo.sync.api.engine.SyncMergeResult.Success @@ -44,6 +43,7 @@ class SavedSitesSyncPersister @Inject constructor( private val savedSitesSyncStore: SavedSitesSyncStore, private val algorithm: SavedSitesSyncPersisterAlgorithm, private val savedSitesFormFactorSyncMigration: SavedSitesFormFactorSyncMigration, + private val savedSitesSyncState: SavedSitesSyncFeatureListener, ) : SyncableDataPersister { override fun onSuccess( @@ -51,8 +51,8 @@ class SavedSitesSyncPersister @Inject constructor( conflictResolution: SyncConflictResolution, ): SyncMergeResult { return if (changes.type == BOOKMARKS) { - savedSitesSyncStore.limitExceeded = false Timber.d("Sync-Bookmarks: received remote changes $changes, merging with resolution $conflictResolution") + savedSitesSyncState.onSuccess(changes) val result = process(changes, conflictResolution) Timber.d("Sync-Bookmarks: merging bookmarks finished with $result") result @@ -62,9 +62,8 @@ class SavedSitesSyncPersister @Inject constructor( } override fun onError(error: SyncErrorResponse) { - when (error.featureSyncError) { - COLLECTION_LIMIT_REACHED -> savedSitesSyncStore.limitExceeded = true - else -> { /* no-op */ } + if (error.type == BOOKMARKS) { + savedSitesSyncState.onError(error.featureSyncError) } } @@ -73,6 +72,7 @@ class SavedSitesSyncPersister @Inject constructor( savedSitesSyncStore.clientModifiedSince = "0" savedSitesSyncStore.startTimeStamp = "0" savedSitesFormFactorSyncMigration.onFormFactorFavouritesDisabled() + savedSitesSyncState.onSyncDisabled() } fun process( diff --git a/saved-sites/saved-sites-impl/src/main/res/layout/notification_rate_limit.xml b/saved-sites/saved-sites-impl/src/main/res/layout/notification_rate_limit.xml new file mode 100644 index 000000000000..8a8ada1ea339 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/layout/notification_rate_limit.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/layout/view_save_site_rate_limit_warning.xml b/saved-sites/saved-sites-impl/src/main/res/layout/view_save_site_rate_limit_warning.xml new file mode 100644 index 000000000000..b22a5803c939 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/layout/view_save_site_rate_limit_warning.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/values/donottranslate.xml b/saved-sites/saved-sites-impl/src/main/res/values/donottranslate.xml index d63307b8d390..07f9f009e258 100644 --- a/saved-sites/saved-sites-impl/src/main/res/values/donottranslate.xml +++ b/saved-sites/saved-sites-impl/src/main/res/values/donottranslate.xml @@ -18,4 +18,6 @@ Share Favorites Use the same favorites on all devices. Leave off to keep mobile and desktop favorites separate. + Sync Paused\nBookmark limit exceeded. Delete some to resume syncing.\n\nManage Bookmarks + Bookmarks Sync is Paused\nYou have exceeded the bookmarks sync limit. Try deleting some bookmarks. Until this is resolved your bookmarks will not be backed up. \ No newline at end of file diff --git a/sync/sync-api/src/main/java/com/duckduckgo/sync/api/SyncMessagePlugin.kt b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/SyncMessagePlugin.kt new file mode 100644 index 000000000000..8675b889dcb5 --- /dev/null +++ b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/SyncMessagePlugin.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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 com.duckduckgo.sync.api + +import android.content.Context +import android.view.View + +interface SyncMessagePlugin { + fun getView(context: Context): View +} diff --git a/sync/sync-api/src/main/java/com/duckduckgo/sync/api/SyncNotificationChannel.kt b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/SyncNotificationChannel.kt new file mode 100644 index 000000000000..b563adf17c6a --- /dev/null +++ b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/SyncNotificationChannel.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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 com.duckduckgo.sync.api + +const val SYNC_NOTIFICATION_CHANNEL_ID = "com.duckduckgo.sync" diff --git a/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/Models.kt b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/Models.kt index c584a78d7caf..b67de9bcdedc 100644 --- a/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/Models.kt +++ b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/Models.kt @@ -60,7 +60,7 @@ data class SyncChangesResponse( } } -data class SyncErrorResponse ( +data class SyncErrorResponse( val type: SyncableType, val featureSyncError: FeatureSyncError, ) @@ -95,7 +95,6 @@ sealed class SyncMergeResult { } } -//this can be removed from here, only used by saved sites? sealed class SyncDataValidationResult { data class Success(val data: T) : SyncDataValidationResult() diff --git a/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncableDataPersister.kt b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncableDataPersister.kt index 97ea6cac8fa0..d929d365e37d 100644 --- a/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncableDataPersister.kt +++ b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncableDataPersister.kt @@ -22,7 +22,7 @@ interface SyncableDataPersister { * Changes from Sync Client have been received * Each feature is responsible for merging and solving conflicts */ - fun persist( + fun onSuccess( changes: SyncChangesResponse, conflictResolution: SyncConflictResolution, ): SyncMergeResult diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt index 4cc999d45a63..c5773091ef4a 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt @@ -23,7 +23,6 @@ import com.duckduckgo.di.scopes.* import com.duckduckgo.sync.api.engine.* import com.duckduckgo.sync.api.engine.SyncEngine.SyncTrigger.ACCOUNT_CREATION import com.duckduckgo.sync.api.engine.SyncEngine.SyncTrigger.ACCOUNT_LOGIN -import com.duckduckgo.sync.api.engine.FeatureSyncError.COLLECTION_LIMIT_REACHED import com.duckduckgo.sync.crypto.* import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.pixels.* @@ -407,15 +406,7 @@ sealed class Result { data class Error( val code: Int = -1, val reason: String, - ) : Result() { - fun getError(): FeatureSyncError { - return when (code) { - API_CODE.COUNT_LIMIT.code -> COLLECTION_LIMIT_REACHED - API_CODE.CONTENT_TOO_LARGE.code -> COLLECTION_LIMIT_REACHED - else -> FeatureSyncError.GENERIC_ERROR - } - } - } + ) : Result() override fun toString(): String { return when (this) { diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncNotificationChannelPlugin.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncNotificationChannelPlugin.kt new file mode 100644 index 000000000000..c6aaf7020875 --- /dev/null +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncNotificationChannelPlugin.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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 com.duckduckgo.sync.impl + +import androidx.core.app.NotificationManagerCompat +import com.duckduckgo.app.notification.model.Channel +import com.duckduckgo.app.notification.model.NotificationPlugin +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.sync.api.SYNC_NOTIFICATION_CHANNEL_ID +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +class SyncNotificationChannelPlugin @Inject constructor() : NotificationPlugin { + override fun getChannels(): List { + return listOf( + SyncNotificationChannelType.SYNC_STATE, + ) + } +} + +internal object SyncNotificationChannelType { + val SYNC_STATE = Channel( + SYNC_NOTIFICATION_CHANNEL_ID, + R.string.sync_notification_channel_name, + NotificationManagerCompat.IMPORTANCE_HIGH, + ) +} diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncService.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncService.kt index 55fb1c703649..69fcf33de138 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncService.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncService.kt @@ -171,6 +171,4 @@ enum class API_CODE(val code: Int) { NOT_MODIFIED(304), COUNT_LIMIT(409), CONTENT_TOO_LARGE(413), - BAD_REQUEST(400), - TOO_MANY_REQUESTS(429), } diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/di/SyncMessageModule.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/di/SyncMessageModule.kt new file mode 100644 index 000000000000..14a47523ae16 --- /dev/null +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/di/SyncMessageModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * 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 com.duckduckgo.sync.impl.di + +import com.duckduckgo.di.* +import com.duckduckgo.di.scopes.* +import com.duckduckgo.sync.api.* +import com.squareup.anvil.annotations.* +import dagger.* +import dagger.multibindings.* + +@Module +@ContributesTo(ActivityScope::class) +abstract class SyncMessageModule { + // we use multibinds as the list of plugins can be empty + @Multibinds + abstract fun provideSyncMessagePlugins(): DaggerSet +} diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/engine/RealSyncEngine.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/engine/RealSyncEngine.kt index 21c4c0c0e91a..b5d0f2aaa80f 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/engine/RealSyncEngine.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/engine/RealSyncEngine.kt @@ -19,6 +19,7 @@ package com.duckduckgo.sync.impl.engine import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.sync.api.engine.* +import com.duckduckgo.sync.api.engine.FeatureSyncError.COLLECTION_LIMIT_REACHED import com.duckduckgo.sync.api.engine.SyncEngine.SyncTrigger import com.duckduckgo.sync.api.engine.SyncEngine.SyncTrigger.ACCOUNT_CREATION import com.duckduckgo.sync.api.engine.SyncEngine.SyncTrigger.ACCOUNT_LOGIN @@ -31,6 +32,7 @@ import com.duckduckgo.sync.api.engine.SyncableDataPersister.SyncConflictResoluti import com.duckduckgo.sync.api.engine.SyncableDataPersister.SyncConflictResolution.LOCAL_WINS import com.duckduckgo.sync.api.engine.SyncableDataPersister.SyncConflictResolution.REMOTE_WINS import com.duckduckgo.sync.api.engine.SyncableDataPersister.SyncConflictResolution.TIMESTAMP +import com.duckduckgo.sync.impl.API_CODE import com.duckduckgo.sync.impl.Result.Error import com.duckduckgo.sync.impl.Result.Success import com.duckduckgo.sync.impl.engine.SyncOperation.DISCARD @@ -174,9 +176,11 @@ class RealSyncEngine @Inject constructor( return when (val result = syncApiClient.patch(changes)) { is Error -> { syncPixels.fireSyncAttemptErrorPixel(changes.type.toString(), result) + val featureError = result.featureError() ?: return persisterPlugins.getPlugins().forEach { - it.onError(SyncErrorResponse(changes.type, result.getError())) + it.onError(SyncErrorResponse(changes.type, featureError)) } + return } is Success -> { @@ -212,7 +216,7 @@ class RealSyncEngine @Inject constructor( conflictResolution: SyncConflictResolution, ) { persisterPlugins.getPlugins().map { - when (val result = it.persist(remoteChanges, conflictResolution)) { + when (val result = it.onSuccess(remoteChanges, conflictResolution)) { is SyncMergeResult.Success -> { if (result.orphans) { syncPixels.fireOrphanPresentPixel(remoteChanges.type.toString()) @@ -231,4 +235,12 @@ class RealSyncEngine @Inject constructor( it.onSyncDisabled() } } + + private fun Error.featureError(): FeatureSyncError? { + return when (code) { + API_CODE.COUNT_LIMIT.code -> COLLECTION_LIMIT_REACHED + API_CODE.CONTENT_TOO_LARGE.code -> COLLECTION_LIMIT_REACHED + else -> null + } + } } diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt index 7cb457d33bc9..4e712dc4b005 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncActivity.kt @@ -19,6 +19,7 @@ package com.duckduckgo.sync.impl.ui import android.app.Activity import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope @@ -96,6 +97,9 @@ class SyncActivity : DuckDuckGoActivity() { @Inject lateinit var syncSettingsPlugin: DaggerMap + @Inject + lateinit var syncFeatureMessagesPlugin: DaggerSet + private val loginFlow = registerForActivityResult(LoginContract()) { resultOk -> if (resultOk) { viewModel.onLoginSuccess() @@ -131,6 +135,7 @@ class SyncActivity : DuckDuckGoActivity() { observeUiEvents() registerForPermission() configureSettings() + configureMessageWarnings() setupClickListeners() setupRecyclerView() @@ -154,6 +159,19 @@ class SyncActivity : DuckDuckGoActivity() { } } + private fun configureMessageWarnings() { + if (syncFeatureMessagesPlugin.isEmpty()) { + binding.viewSyncEnabled.syncFeatureWarningsContainer.isVisible = false + } else { + syncFeatureMessagesPlugin.forEach { plugin -> + plugin.getView(this)?.let { view -> + binding.viewSyncEnabled.syncFeatureWarningsContainer.addView(view) + } + binding.viewSyncEnabled.syncFeatureWarningsContainer.isVisible = true + } + } + } + private fun setupClickListeners() { binding.viewSyncDisabled.syncSetupScanQr.setClickListener { viewModel.onScanQRCodeClicked() diff --git a/sync/sync-impl/src/main/res/layout/view_sync_enabled.xml b/sync/sync-impl/src/main/res/layout/view_sync_enabled.xml index e8aced776618..5a29ed6b21bc 100644 --- a/sync/sync-impl/src/main/res/layout/view_sync_enabled.xml +++ b/sync/sync-impl/src/main/res/layout/view_sync_enabled.xml @@ -30,6 +30,12 @@ app:leadingIconBackground="circular" app:primaryText="@string/sync_disable_sync_item" /> + + diff --git a/sync/sync-impl/src/main/res/values/donottranslate.xml b/sync/sync-impl/src/main/res/values/donottranslate.xml index 20e35e7a4edc..fbbd891cfbaa 100644 --- a/sync/sync-impl/src/main/res/values/donottranslate.xml +++ b/sync/sync-impl/src/main/res/values/donottranslate.xml @@ -121,4 +121,5 @@ Please go to your device\'s settings and grant permission for this app to access your camera. Go to Settings Something went wrong + Sync \ No newline at end of file diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/FakeSyncableDataPersister.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/FakeSyncableDataPersister.kt index 0f8e58329f72..31e34861777f 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/FakeSyncableDataPersister.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/FakeSyncableDataPersister.kt @@ -24,7 +24,7 @@ import com.duckduckgo.sync.api.engine.SyncableDataPersister.SyncConflictResoluti class FakeSyncableDataPersister(private val orphans: Boolean = false) : SyncableDataPersister { - override fun persist( + override fun onSuccess( changes: SyncChangesResponse, conflictResolution: SyncConflictResolution, ): SyncMergeResult { diff --git a/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersister.kt b/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersister.kt index 37bb014195a4..95843247cbe1 100644 --- a/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersister.kt +++ b/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersister.kt @@ -43,7 +43,7 @@ class SettingsSyncDataPersister @Inject constructor( val syncCrypto: SyncCrypto, private val dispatchers: DispatcherProvider, ) : SyncableDataPersister { - override fun persist( + override fun onSuccess( changes: SyncChangesResponse, conflictResolution: SyncConflictResolution, ): SyncMergeResult { @@ -59,7 +59,7 @@ class SettingsSyncDataPersister @Inject constructor( } override fun onError(error: SyncErrorResponse) { - TODO("Not yet implemented") + // no-op } private suspend fun process( diff --git a/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersisterTest.kt b/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersisterTest.kt index 626248b5d720..d6924088ffcc 100644 --- a/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersisterTest.kt +++ b/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersisterTest.kt @@ -69,7 +69,7 @@ class SettingsSyncDataPersisterTest { @Test fun whenPersistChangesDeduplicationWithdValueThenCallDeduplicateWithValue() { - val result = testee.persist( + val result = testee.onSuccess( changes = SyncChangesResponse( type = SyncableType.SETTINGS, jsonString = responseWithValuesObject, @@ -83,7 +83,7 @@ class SettingsSyncDataPersisterTest { @Test fun whenPersistChangesDeduplicationWithDeletedValueThenCallDeduplicateWithNull() { - val result = testee.persist( + val result = testee.onSuccess( changes = SyncChangesResponse( type = SyncableType.SETTINGS, jsonString = responseWithDeletedObject, @@ -98,7 +98,7 @@ class SettingsSyncDataPersisterTest { @Test fun whenPersistChangesTimestampAndNoRecentChangeThenCallMergeWithValue() { settingSyncStore.startTimeStamp = "2023-08-31T10:06:16.022Z" - val result = testee.persist( + val result = testee.onSuccess( changes = SyncChangesResponse( type = SyncableType.SETTINGS, jsonString = responseWithValuesObject, @@ -112,7 +112,7 @@ class SettingsSyncDataPersisterTest { @Test fun whenPersistChangesTimestampWithDeletedValueThenCallSaveWithNull() { - val result = testee.persist( + val result = testee.onSuccess( changes = SyncChangesResponse( type = SyncableType.SETTINGS, jsonString = responseWithDeletedObject, @@ -135,7 +135,7 @@ class SettingsSyncDataPersisterTest { ), ) - val result = testee.persist( + val result = testee.onSuccess( changes = SyncChangesResponse( type = SyncableType.SETTINGS, jsonString = responseWithValuesObject, @@ -151,7 +151,7 @@ class SettingsSyncDataPersisterTest { fun whenPersistChangesSucceedsThenUpdateServerAndClientTimestamps() { settingSyncStore.startTimeStamp = "2023-08-31T10:06:16.022Z" - val result = testee.persist( + val result = testee.onSuccess( changes = SyncChangesResponse( type = SyncableType.SETTINGS, jsonString = responseWithValuesObject,