From 8434f37e5dfe3e719383552c2c500bda3bcc1af1 Mon Sep 17 00:00:00 2001 From: Karl Dimla Date: Mon, 12 Feb 2024 18:11:54 +0100 Subject: [PATCH 01/19] Fix binding for SubsNetpDisabledNotificationBuilder (#4178) Task/Issue URL: https://app.asana.com/0/488551667048375/1206579032673527/f ### Description Update scope ### Steps to test this PR QA optional --- .../notification/SubsNetpDisabledNotificationBuilder.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/notification/SubsNetpDisabledNotificationBuilder.kt b/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/notification/SubsNetpDisabledNotificationBuilder.kt index 82b8c11a09c1..f7c7c5416cc1 100644 --- a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/notification/SubsNetpDisabledNotificationBuilder.kt +++ b/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/notification/SubsNetpDisabledNotificationBuilder.kt @@ -18,7 +18,7 @@ package com.duckduckgo.networkprotection.subscription.notification import android.app.Notification import android.content.Context -import com.duckduckgo.di.scopes.VpnScope +import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.networkprotection.impl.notification.NetPDisabledNotificationBuilder import com.duckduckgo.networkprotection.impl.notification.RealNetPDisabledNotificationBuilder import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository @@ -27,7 +27,7 @@ import com.squareup.anvil.annotations.ContributesBinding.Priority.HIGHEST import javax.inject.Inject @ContributesBinding( - scope = VpnScope::class, + scope = AppScope::class, priority = HIGHEST, ) class SubsNetpDisabledNotificationBuilder @Inject constructor( From 17ae4a8756d0722619807ce6779caf05aa114c02 Mon Sep 17 00:00:00 2001 From: Aitor Viana Date: Tue, 13 Feb 2024 09:43:33 +0000 Subject: [PATCH 02/19] Custom DNS VPN setting feature (internal only) (#4172) Task/Issue URL: https://app.asana.com/0/42792087274227/1206484244032149/f ### Description Add Custom DNS VPN setting for internal builds only ### Steps to test this PR Download an app like dnsleak or similar - [x] install from this branch and enable VPN - [x] run dns leak - [x] verify the DNS is our VPN DNS - [x] to go VPN management screen -> VPN settings -> custom DNS - [x] set a custom DNS, eg. 1.1.1.1 and tap `Apply` - [x] run dns leak - [x] verify the DNS is Cloudflare's - [x] try to set a custom DNS that is a local IP address, eg. anything on 10/8, 172.16/12, and 192.168/16 - [x] verify the `Apply` button is not interact-able - [x] change the custom DNS config to either default or another custom DNS - [x] verify changes only take effect when `Apply` button is tapped --- .../vpn/network/FakeVpnNetworkStack.kt | 2 +- .../android/vpn/network/VpnNetworkStack.kt | 1 + .../vpn/integration/NgVpnNetworkStack.kt | 1 + .../vpn/service/TrackerBlockingVpnService.kt | 4 +- .../common/ui/view/text/DaxTextInput.kt | 10 +- .../src/main/res/values/attrs-text-input.xml | 1 + .../impl/WgVpnNetworkStack.kt | 3 + .../impl/WgVpnNetworkStackTest.kt | 2 + .../src/main/AndroidManifest.xml | 5 + .../internal/feature/FeaturePriorities.kt | 1 + .../custom_dns/VpnCustomDnsActivity.kt | 158 ++++++++++++++++++ .../custom_dns/VpnCustomDnsSettingView.kt | 132 +++++++++++++++ .../custom_dns/VpnCustomDnsViewModel.kt | 97 +++++++++++ .../VpnCustomDnsViewSettingViewModel.kt | 59 +++++++ .../NetPInternalDefaultConfigProvider.kt | 5 +- .../network/NetPInternalEnvDataStore.kt | 15 +- .../res/layout/activity_netp_custom_dns.xml | 104 ++++++++++++ .../layout/vpn_view_settings_custom_dns.xml | 26 +++ .../src/main/res/values/donottranslate.xml | 6 + 19 files changed, 622 insertions(+), 10 deletions(-) create mode 100644 network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsActivity.kt create mode 100644 network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsSettingView.kt create mode 100644 network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsViewModel.kt create mode 100644 network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsViewSettingViewModel.kt create mode 100644 network-protection/network-protection-internal/src/main/res/layout/activity_netp_custom_dns.xml create mode 100644 network-protection/network-protection-internal/src/main/res/layout/vpn_view_settings_custom_dns.xml diff --git a/app-tracking-protection/vpn-api-test/src/main/java/com/duckduckgo/mobile/android/vpn/network/FakeVpnNetworkStack.kt b/app-tracking-protection/vpn-api-test/src/main/java/com/duckduckgo/mobile/android/vpn/network/FakeVpnNetworkStack.kt index 2698a14f1d5f..e25ff39e70f5 100644 --- a/app-tracking-protection/vpn-api-test/src/main/java/com/duckduckgo/mobile/android/vpn/network/FakeVpnNetworkStack.kt +++ b/app-tracking-protection/vpn-api-test/src/main/java/com/duckduckgo/mobile/android/vpn/network/FakeVpnNetworkStack.kt @@ -25,7 +25,7 @@ class FakeVpnNetworkStack(override val name: String) : VpnNetworkStack { } override suspend fun onPrepareVpn(): Result { - return Result.success(VpnNetworkStack.VpnTunnelConfig(1500, emptyMap(), emptySet(), emptyMap(), emptySet())) + return Result.success(VpnNetworkStack.VpnTunnelConfig(1500, emptyMap(), emptySet(), emptySet(), emptyMap(), emptySet())) } override fun onStartVpn(tunfd: ParcelFileDescriptor): Result { diff --git a/app-tracking-protection/vpn-api/src/main/java/com/duckduckgo/mobile/android/vpn/network/VpnNetworkStack.kt b/app-tracking-protection/vpn-api/src/main/java/com/duckduckgo/mobile/android/vpn/network/VpnNetworkStack.kt index 78bce1ba5ed8..536906547477 100644 --- a/app-tracking-protection/vpn-api/src/main/java/com/duckduckgo/mobile/android/vpn/network/VpnNetworkStack.kt +++ b/app-tracking-protection/vpn-api/src/main/java/com/duckduckgo/mobile/android/vpn/network/VpnNetworkStack.kt @@ -77,6 +77,7 @@ interface VpnNetworkStack { val mtu: Int, val addresses: Map, val dns: Set, + val customDns: Set, val routes: Map, val appExclusionList: Set, ) diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/integration/NgVpnNetworkStack.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/integration/NgVpnNetworkStack.kt index e15b6881c3cc..5f2635924f7a 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/integration/NgVpnNetworkStack.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/integration/NgVpnNetworkStack.kt @@ -123,6 +123,7 @@ class NgVpnNetworkStack @Inject constructor( InetAddress.getByName("fd00:1:fd00:1:fd00:1:fd00:1") to 128, // Add IPv6 Unique Local Address ), dns = getDns(), + customDns = emptySet(), routes = emptyMap(), appExclusionList = trackingProtectionAppsRepository.getExclusionAppsList().toSet(), ), diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt index c9ef945c1a59..f88444c10b56 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt @@ -443,7 +443,9 @@ class TrackerBlockingVpnService : VpnService(), CoroutineScope by MainScope(), V allowFamily(AF_INET6) } - val dnsToConfigure = checkAndReturnDns(tunnelConfig.dns) + val tunnelDns = checkAndReturnDns(tunnelConfig.dns) + val customDns = checkAndReturnDns(tunnelConfig.customDns).filterIsInstance() + val dnsToConfigure = customDns.ifEmpty { tunnelDns } // TODO: eventually routes will be set by remote config if (appBuildConfig.isPerformanceTest && appBuildConfig.isInternalBuild()) { diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/text/DaxTextInput.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/text/DaxTextInput.kt index 008247789201..0c6b4b1d13c3 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/text/DaxTextInput.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/text/DaxTextInput.kt @@ -27,6 +27,7 @@ import android.text.InputType import android.text.TextUtils.TruncateAt import android.text.TextUtils.TruncateAt.END import android.text.TextWatcher +import android.text.method.DigitsKeyListener import android.text.method.PasswordTransformationMethod import android.util.AttributeSet import android.util.SparseArray @@ -43,6 +44,7 @@ import androidx.core.view.updateLayoutParams import androidx.core.widget.doOnTextChanged import com.duckduckgo.common.ui.view.showKeyboard import com.duckduckgo.common.ui.view.text.DaxTextInput.Type.INPUT_TYPE_FORM_MODE +import com.duckduckgo.common.ui.view.text.DaxTextInput.Type.INPUT_TYPE_IP_ADDRESS_MODE import com.duckduckgo.common.ui.view.text.DaxTextInput.Type.INPUT_TYPE_MULTI_LINE import com.duckduckgo.common.ui.view.text.DaxTextInput.Type.INPUT_TYPE_PASSWORD import com.duckduckgo.common.ui.view.text.DaxTextInput.Type.INPUT_TYPE_SINGLE_LINE @@ -54,6 +56,7 @@ import com.duckduckgo.mobile.android.databinding.ViewDaxTextInputBinding import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE +import java.util.* interface TextInput { var text: String @@ -181,6 +184,7 @@ class DaxTextInput @JvmOverloads constructor( get() = binding.internalEditText.text.toString() set(value) { binding.internalEditText.setText(value) + binding.internalEditText.setSelection(value.length) } override var hint: String @@ -341,7 +345,10 @@ class DaxTextInput @JvmOverloads constructor( binding.internalEditText.ellipsize = TruncateAt.END } - if (inputType == INPUT_TYPE_MULTI_LINE || inputType == INPUT_TYPE_FORM_MODE) { + if (inputType == INPUT_TYPE_IP_ADDRESS_MODE) { + binding.internalEditText.inputType = EditorInfo.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_FLAG_DECIMAL + binding.internalEditText.keyListener = DigitsKeyListener.getInstance("0123456789.") + } else if (inputType == INPUT_TYPE_MULTI_LINE || inputType == INPUT_TYPE_FORM_MODE) { binding.internalEditText.inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE } else { binding.internalEditText.inputType = EditorInfo.TYPE_CLASS_TEXT @@ -402,6 +409,7 @@ class DaxTextInput @JvmOverloads constructor( INPUT_TYPE_SINGLE_LINE(1), INPUT_TYPE_PASSWORD(2), INPUT_TYPE_FORM_MODE(3), + INPUT_TYPE_IP_ADDRESS_MODE(4), } companion object { diff --git a/common/common-ui/src/main/res/values/attrs-text-input.xml b/common/common-ui/src/main/res/values/attrs-text-input.xml index 039796001a86..ac406f1a9ebb 100644 --- a/common/common-ui/src/main/res/values/attrs-text-input.xml +++ b/common/common-ui/src/main/res/values/attrs-text-input.xml @@ -31,6 +31,7 @@ + diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/WgVpnNetworkStack.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/WgVpnNetworkStack.kt index d7e5e813829a..ba86f62e91f7 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/WgVpnNetworkStack.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/WgVpnNetworkStack.kt @@ -26,6 +26,7 @@ import com.duckduckgo.mobile.android.vpn.network.VpnNetworkStack.VpnTunnelConfig import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.RESTART import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.SELF_STOP +import com.duckduckgo.networkprotection.impl.config.NetPDefaultConfigProvider import com.duckduckgo.networkprotection.impl.configuration.WgTunnel import com.duckduckgo.networkprotection.impl.configuration.WgTunnelConfig import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixels @@ -49,6 +50,7 @@ class WgVpnNetworkStack @Inject constructor( private val wgTunnelLazy: Lazy, private val wgTunnelConfigLazy: Lazy, private val networkProtectionRepository: Lazy, + private val netPDefaultConfigProvider: NetPDefaultConfigProvider, private val currentTimeProvider: CurrentTimeProvider, private val netpPixels: Lazy, private val dnsProvider: DnsProvider, @@ -78,6 +80,7 @@ class WgVpnNetworkStack @Inject constructor( // why? no use intercepting encrypted DNS traffic, plus we can't configure any DNS that doesn't support DoT, otherwise Android // will enforce DoT and will stop passing any DNS traffic, resulting in no DNS resolution == connectivity is killed dns = if (privateDns.isEmpty()) wgConfig!!.`interface`.dnsServers else emptySet(), + customDns = netPDefaultConfigProvider.fallbackDns(), routes = wgConfig!!.`interface`.routes.associate { it.address.hostAddress!! to it.mask }, appExclusionList = wgConfig!!.`interface`.excludedApplications, ), diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/WgVpnNetworkStackTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/WgVpnNetworkStackTest.kt index ca96e413ac99..29d2c37c8afb 100644 --- a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/WgVpnNetworkStackTest.kt +++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/WgVpnNetworkStackTest.kt @@ -114,6 +114,7 @@ class WgVpnNetworkStackTest { { wgTunnel }, { wgTunnelConfig }, { networkProtectionRepository }, + netPDefaultConfigProvider, currentTimeProvider, { netpPixels }, privateDnsProvider, @@ -274,6 +275,7 @@ class WgVpnNetworkStackTest { // why? no use intercepting encrypted DNS traffic, plus we can't configure any DNS that doesn't support DoT, otherwise Android // will enforce DoT and will stop passing any DNS traffic, resulting in no DNS resolution == connectivity is killed dns = this.`interface`.dnsServers, + customDns = netPDefaultConfigProvider.fallbackDns(), routes = this.`interface`.routes.associate { it.address.hostAddress!! to it.mask }, appExclusionList = this.`interface`.excludedApplications, ) diff --git a/network-protection/network-protection-internal/src/main/AndroidManifest.xml b/network-protection/network-protection-internal/src/main/AndroidManifest.xml index 745328842e78..0a37ac902b28 100644 --- a/network-protection/network-protection-internal/src/main/AndroidManifest.xml +++ b/network-protection/network-protection-internal/src/main/AndroidManifest.xml @@ -24,6 +24,11 @@ android:process=":vpn" android:label="@string/netpEnvironmentSetting" android:exported="false" /> + \ No newline at end of file diff --git a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/FeaturePriorities.kt b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/FeaturePriorities.kt index 6e18de732005..44906817962c 100644 --- a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/FeaturePriorities.kt +++ b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/FeaturePriorities.kt @@ -21,3 +21,4 @@ private const val INTERNAL_SETTING_BASE = 10000 internal const val INTERNAL_SETTING_SEPARATOR = INTERNAL_SETTING_BASE + 10 internal const val INTERNAL_SETTING_HEADING = INTERNAL_SETTING_BASE + 20 internal const val UNSAFE_WIFI_DETECTION_PRIORITY = INTERNAL_SETTING_BASE + 30 +internal const val CUSTOM_DNS_PRIORITY = INTERNAL_SETTING_BASE + 40 diff --git a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsActivity.kt b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsActivity.kt new file mode 100644 index 000000000000..76e006a9b525 --- /dev/null +++ b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsActivity.kt @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2024 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.networkprotection.internal.feature.custom_dns + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.widget.CompoundButton.OnCheckedChangeListener +import androidx.lifecycle.lifecycleScope +import com.duckduckgo.anvil.annotations.ContributeToActivityStarter +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.common.ui.DuckDuckGoActivity +import com.duckduckgo.common.ui.view.quietlySetIsChecked +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams +import com.duckduckgo.networkprotection.api.NetworkProtectionState +import com.duckduckgo.networkprotection.internal.databinding.ActivityNetpCustomDnsBinding +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.Event.Init +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.Event.OnApply +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.State.CustomDns +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.State.DefaultDns +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.State.Done +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.State.NeedApply +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch + +@InjectWith(ActivityScope::class) +@ContributeToActivityStarter(VpnCustomDnsScreen.Default::class) +class VpnCustomDnsActivity : DuckDuckGoActivity() { + + private val binding: ActivityNetpCustomDnsBinding by viewBinding() + private val viewModel: VpnCustomDnsViewModel by bindViewModel() + + private val events = MutableSharedFlow(replay = 1, extraBufferCapacity = 1) + + private val defaultDnsListener = OnCheckedChangeListener { button, value -> + if (value) { + lifecycleScope.launch { + events.emit(Event.DefaultDnsSelected) + } + } + } + + private val customDnsListener = OnCheckedChangeListener { button, value -> + if (value) { + lifecycleScope.launch { + events.emit(Event.CustomDnsEntered(binding.customDns.text)) + } + } + } + + private val customDnsTextWatcher = object : TextWatcher { + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} + + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} + + override fun afterTextChanged(p0: Editable?) { + lifecycleScope.launch { + events.emit(Event.CustomDnsEntered(p0.toString())) + } + } + } + + @Inject lateinit var dispatcherProvider: DispatcherProvider + + @Inject lateinit var networkProtectionState: NetworkProtectionState + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + setupToolbar(binding.toolbar) + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun onResume() { + super.onResume() + lifecycleScope.launch { + events + .flatMapLatest { viewModel.reduce(it) } + .flowOn(dispatcherProvider.io()) + .onStart { events.emit(Init) } + .collect(::render) + } + binding.defaultDnsOption.setOnCheckedChangeListener(defaultDnsListener) + binding.customDnsOption.setOnCheckedChangeListener(customDnsListener) + binding.customDns.addTextChangedListener(customDnsTextWatcher) + binding.applyCustomDns.setOnClickListener { + lifecycleScope.launch { + events.emit(OnApply) + } + } + } + + private fun render(state: State) { + when (state) { + DefaultDns -> { + binding.defaultDnsOption.quietlySetIsChecked(true, defaultDnsListener) + binding.customDns.removeTextChangedListener(customDnsTextWatcher) + binding.customDns.isEditable = false + binding.customDns.addTextChangedListener(customDnsTextWatcher) + } + is CustomDns -> { + binding.customDnsOption.quietlySetIsChecked(true, customDnsListener) + binding.customDns.removeTextChangedListener(customDnsTextWatcher) + binding.customDns.text = state.dns + binding.customDns.isEditable = true + binding.customDns.addTextChangedListener(customDnsTextWatcher) + } + + is NeedApply -> binding.applyCustomDns.isEnabled = state.value + Done -> { + networkProtectionState.restart() + finish() + } + } + } + + internal sealed class Event { + data object Init : Event() + data class CustomDnsEntered(val dns: String?) : Event() + data object DefaultDnsSelected : Event() + data object OnApply : Event() + } + + internal sealed class State { + data class NeedApply(val value: Boolean) : State() + data object DefaultDns : State() + data class CustomDns(val dns: String) : State() + data object Done : State() + } +} + +sealed class VpnCustomDnsScreen { + data object Default : ActivityParams { + private fun readResolve(): Any = Default + } +} diff --git a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsSettingView.kt b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsSettingView.kt new file mode 100644 index 000000000000..4685b36df0ec --- /dev/null +++ b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsSettingView.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2024 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.networkprotection.internal.feature.custom_dns + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewTreeLifecycleOwner +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.findViewTreeViewModelStoreOwner +import androidx.lifecycle.lifecycleScope +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.anvil.annotations.PriorityKey +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.di.scopes.ViewScope +import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.networkprotection.impl.settings.VpnSettingPlugin +import com.duckduckgo.networkprotection.internal.R +import com.duckduckgo.networkprotection.internal.databinding.VpnViewSettingsCustomDnsBinding +import com.duckduckgo.networkprotection.internal.feature.CUSTOM_DNS_PRIORITY +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsSettingView.Event.Init +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsSettingView.State.CustomDns +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsSettingView.State.Default +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsSettingView.State.Idle +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsViewSettingViewModel.Factory +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.android.support.AndroidSupportInjection +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch + +@InjectWith(ViewScope::class) +class VpnCustomDnsSettingView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : FrameLayout(context, attrs, defStyle) { + + @Inject + lateinit var viewModelFactory: Factory + + @Inject + lateinit var dispatcherProvider: DispatcherProvider + + @Inject + lateinit var globalActivityStarter: GlobalActivityStarter + + private val events = MutableSharedFlow(replay = 1, extraBufferCapacity = 1) + + private val binding: VpnViewSettingsCustomDnsBinding by viewBinding() + + private val viewModel: VpnCustomDnsViewSettingViewModel by lazy { + ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[VpnCustomDnsViewSettingViewModel::class.java] + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun onAttachedToWindow() { + AndroidSupportInjection.inject(this) + super.onAttachedToWindow() + + binding.customDnsSetting.setOnClickListener { + globalActivityStarter.start(context, VpnCustomDnsScreen.Default) + } + + ViewTreeLifecycleOwner.get(this)?.lifecycleScope?.launch { + events + .flatMapLatest { viewModel.reduce(it) } + .flowOn(dispatcherProvider.io()) + .onStart { events.emit(Init) } + .collect(::render) + } + } + + override fun onVisibilityChanged( + changedView: View, + visibility: Int, + ) { + super.onVisibilityChanged(changedView, visibility) + findViewTreeLifecycleOwner()?.lifecycleScope?.launch { + // every time is visible, emit to refresh state + if (visibility == 0) events.emit(Init) + } + } + + private fun render(state: State) { + when (state) { + Idle -> { } + CustomDns -> binding.customDnsSetting.setSecondaryText(context.getString(R.string.netpCustomDnsCustom)) + Default -> binding.customDnsSetting.setSecondaryText(context.getString(R.string.netpCustomDnsDefault)) + } + } + + sealed class Event { + data object Init : Event() + } + + sealed class State { + data object Idle : State() + data object Default : State() + data object CustomDns : State() + } +} + +@ContributesMultibinding(ActivityScope::class) +@PriorityKey(CUSTOM_DNS_PRIORITY) +class VpnCustomDnsSettingViewPlugin @Inject constructor() : VpnSettingPlugin { + override fun getView(context: Context): View { + return VpnCustomDnsSettingView(context) + } +} diff --git a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsViewModel.kt b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsViewModel.kt new file mode 100644 index 000000000000..90684e577923 --- /dev/null +++ b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsViewModel.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 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.networkprotection.internal.feature.custom_dns + +import androidx.lifecycle.ViewModel +import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.Event +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.Event.CustomDnsEntered +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.Event.DefaultDnsSelected +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.Event.Init +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.Event.OnApply +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsActivity.State +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsViewModel.InitialState.CustomDns +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsViewModel.InitialState.DefaultDns +import com.duckduckgo.networkprotection.internal.network.NetPInternalEnvDataStore +import com.wireguard.config.InetAddresses +import java.net.Inet4Address +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +@ContributesViewModel(ActivityScope::class) +class VpnCustomDnsViewModel @Inject constructor( + private val netPInternalEnvDataStore: NetPInternalEnvDataStore, +) : ViewModel() { + + private lateinit var initialState: InitialState + private var currentState: InitialState = DefaultDns + + internal fun reduce(event: Event): Flow { + return when (event) { + Init -> onInit() + DefaultDnsSelected -> handleDefaultDnsSelected() + is CustomDnsEntered -> handleCustomDnsEntered(event) + OnApply -> handleOnApply() + } + } + + private fun handleOnApply() = flow { + when (val currentState = currentState) { // defensive copy + is DefaultDns -> netPInternalEnvDataStore.customDns = null + is CustomDns -> netPInternalEnvDataStore.customDns = currentState.dns + } + emit(State.Done) + } + + private fun handleDefaultDnsSelected() = flow { + currentState = DefaultDns + emit(State.DefaultDns) + emit(State.NeedApply(initialState != currentState)) + } + + private fun handleCustomDnsEntered(event: CustomDnsEntered) = flow { + val dns = event.dns.orEmpty() + currentState = CustomDns(dns) + emit(State.CustomDns(dns)) + val apply = (initialState != currentState) && dns.isValidAddress() + emit(State.NeedApply(apply)) + } + + private fun onInit(): Flow = flow { + val customDns = netPInternalEnvDataStore.customDns + if (!this@VpnCustomDnsViewModel::initialState.isInitialized) { + initialState = customDns?.let { CustomDns(it) } ?: DefaultDns + currentState = initialState + } + customDns?.let { + emit(State.CustomDns(it)) + } ?: emit(State.DefaultDns) + } + + private fun String.isValidAddress(): Boolean { + return runCatching { InetAddresses.parse(this) }.getOrNull()?.let { inetAddress -> + return (inetAddress is Inet4Address) && !inetAddress.isSiteLocalAddress + } ?: false + } + + private sealed class InitialState { + data object DefaultDns : InitialState() + data class CustomDns(val dns: String) : InitialState() + } +} diff --git a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsViewSettingViewModel.kt b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsViewSettingViewModel.kt new file mode 100644 index 000000000000..8f58e86963b0 --- /dev/null +++ b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/custom_dns/VpnCustomDnsViewSettingViewModel.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 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.networkprotection.internal.feature.custom_dns + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsSettingView.Event +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsSettingView.Event.Init +import com.duckduckgo.networkprotection.internal.feature.custom_dns.VpnCustomDnsSettingView.State +import com.duckduckgo.networkprotection.internal.network.NetPInternalEnvDataStore +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class VpnCustomDnsViewSettingViewModel( + private val netPInternalEnvDataStore: NetPInternalEnvDataStore, +) : ViewModel() { + + internal fun reduce(event: Event): Flow { + return when (event) { + Init -> onInit() + } + } + + private fun onInit(): Flow = flow { + netPInternalEnvDataStore.customDns?.let { + emit(State.CustomDns) + } ?: emit(State.Default) + } + + @Suppress("UNCHECKED_CAST") + class Factory @Inject constructor( + private val store: NetPInternalEnvDataStore, + ) : ViewModelProvider.NewInstanceFactory() { + + override fun create(modelClass: Class): T { + return with(modelClass) { + when { + isAssignableFrom(VpnCustomDnsViewSettingViewModel::class.java) -> VpnCustomDnsViewSettingViewModel(store) + else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") + } + } as T + } + } +} diff --git a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/network/NetPInternalDefaultConfigProvider.kt b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/network/NetPInternalDefaultConfigProvider.kt index 486e5705b0b8..13a97433f983 100644 --- a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/network/NetPInternalDefaultConfigProvider.kt +++ b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/network/NetPInternalDefaultConfigProvider.kt @@ -39,6 +39,7 @@ class NetPInternalDefaultConfigProvider @Inject constructor( private val mtuInternalProvider: NetPInternalMtuProvider, private val exclusionListProvider: NetPInternalExclusionListProvider, private val netPInternalFeatureToggles: NetPInternalFeatureToggles, + private val netPInternalEnvDataStore: NetPInternalEnvDataStore, private val context: Context, ) : NetPDefaultConfigProvider { @@ -55,8 +56,8 @@ class NetPInternalDefaultConfigProvider @Inject constructor( override fun fallbackDns(): Set { return realNetPConfigProvider.fallbackDns().toMutableSet().apply { - if (netPInternalFeatureToggles.cloudflareDnsFallback().isEnabled()) { - runCatching { InetAddress.getAllByName("one.one.one.one") }.getOrNull()?.let { + netPInternalEnvDataStore.customDns?.let { dns -> + runCatching { InetAddress.getAllByName(dns) }.getOrNull()?.let { addAll(it) } ?: logcat(LogPriority.ERROR) { "Error resolving fallback DNS" } } diff --git a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/network/NetPInternalEnvDataStore.kt b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/network/NetPInternalEnvDataStore.kt index 6fb40f44387a..a16c45c601b4 100644 --- a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/network/NetPInternalEnvDataStore.kt +++ b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/network/NetPInternalEnvDataStore.kt @@ -16,26 +16,31 @@ package com.duckduckgo.networkprotection.internal.network -import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit +import com.duckduckgo.mobile.android.vpn.prefs.VpnSharedPreferencesProvider import javax.inject.Inject -class NetPInternalEnvDataStore @Inject constructor( - context: Context, -) { +class NetPInternalEnvDataStore @Inject constructor(vpnSharedPreferencesProvider: VpnSharedPreferencesProvider) { + + private val preferences: SharedPreferences by lazy { + vpnSharedPreferencesProvider.getSharedPreferences(FILENAME, multiprocess = true, migrate = false) + } - private val preferences: SharedPreferences by lazy { context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) } var netpCustomEnvironmentUrl: String? get() = preferences.getString(KEY_NETP_ENVIRONMENT_URL, null) set(value) = preferences.edit { putString(KEY_NETP_ENVIRONMENT_URL, value) } var useNetpCustomEnvironmentUrl: Boolean get() = preferences.getBoolean(KEY_NETP_USE_ENVIRONMENT_URL, false) set(enabled) = preferences.edit { putBoolean(KEY_NETP_USE_ENVIRONMENT_URL, enabled) } + var customDns: String? + get() = preferences.getString(KEY_NETP_CUSTOM_DNS, null) + set(value) = preferences.edit { putString(KEY_NETP_CUSTOM_DNS, value) } companion object { private const val FILENAME = "com.duckduckgo.netp.internal.env.store.v1" private const val KEY_NETP_ENVIRONMENT_URL = "KEY_NETP_ENVIRONMENT_URL" private const val KEY_NETP_USE_ENVIRONMENT_URL = "KEY_NETP_USE_ENVIRONMENT_URL" + private const val KEY_NETP_CUSTOM_DNS = "KEY_NETP_CUSTOM_DNS" } } diff --git a/network-protection/network-protection-internal/src/main/res/layout/activity_netp_custom_dns.xml b/network-protection/network-protection-internal/src/main/res/layout/activity_netp_custom_dns.xml new file mode 100644 index 000000000000..b3cc22df0249 --- /dev/null +++ b/network-protection/network-protection-internal/src/main/res/layout/activity_netp_custom_dns.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/network-protection/network-protection-internal/src/main/res/layout/vpn_view_settings_custom_dns.xml b/network-protection/network-protection-internal/src/main/res/layout/vpn_view_settings_custom_dns.xml new file mode 100644 index 000000000000..d87080ea646e --- /dev/null +++ b/network-protection/network-protection-internal/src/main/res/layout/vpn_view_settings_custom_dns.xml @@ -0,0 +1,26 @@ + + + diff --git a/network-protection/network-protection-internal/src/main/res/values/donottranslate.xml b/network-protection/network-protection-internal/src/main/res/values/donottranslate.xml index 0167c7d42c4a..e588d70e27c5 100644 --- a/network-protection/network-protection-internal/src/main/res/values/donottranslate.xml +++ b/network-protection/network-protection-internal/src/main/res/values/donottranslate.xml @@ -22,6 +22,7 @@ Share PCAP file Manage System Apps Exclusion Change NetP Environment + DNS Exclude All System Apps Snooze VPN while calling Detects when the phone is making a call and temporarily snoozes the VPN. @@ -35,4 +36,9 @@ Force Rekey Unsafe Wi-Fi detection Get notified when connected to unsafe Wi-Fi without a VPN. + Default + Custom + Use DuckDuckGo VPN or specify custom DNS server. Only IPv4 public address supported. + Enter DNS server IP address + Apply \ No newline at end of file From ae54d61ade4f131cdd0fcc5fc168a75887be32f5 Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Tue, 13 Feb 2024 17:52:43 +0000 Subject: [PATCH 03/19] Fix the removal of OS_VERSION (#4180) Task/Issue URL: https://app.asana.com/0/488551667048375/1206593509245645/f ### Description Fixed removeOSVersion() in PixelParamRemovalPlugin and updated tests. ### Steps to test this PR - [x] Install from this branch. - [x] Download a file. - [x] Notice the pixel sent has the app version. ### NO UI changes --- .../global/api/PixelParamRemovalInterceptorTest.kt | 13 +++++-------- .../utils/plugins/pixel/PixelParamRemovalPlugin.kt | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/src/test/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptorTest.kt b/app/src/test/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptorTest.kt index bc680dcd3c39..bfc5ccbd3909 100644 --- a/app/src/test/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptorTest.kt +++ b/app/src/test/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptorTest.kt @@ -20,9 +20,6 @@ import com.duckduckgo.common.test.api.FakeChain import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter -import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter.APP_VERSION -import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter.ATB -import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter.OS_VERSION import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before @@ -50,7 +47,7 @@ class PixelParamRemovalInterceptorTest { @Test fun whenSendPixelRedactAppVersion() { - testPixels.filter { it.second == setOf(APP_VERSION) }.map { it.first }.forEach { pixelName -> + testPixels.filter { it.second == PixelParameter.removeVersion() }.map { it.first }.forEach { pixelName -> val pixelUrl = String.format(PIXEL_TEMPLATE, pixelName) val interceptedUrl = pixelRemovalInterceptor.intercept(FakeChain(pixelUrl)).request.url assertNotNull(interceptedUrl.queryParameter("atb")) @@ -60,7 +57,7 @@ class PixelParamRemovalInterceptorTest { @Test fun whenSendPixelRedactAtb() { - testPixels.filter { it.second == setOf(ATB) }.map { it.first }.forEach { pixelName -> + testPixels.filter { it.second == PixelParameter.removeAtb() }.map { it.first }.forEach { pixelName -> val pixelUrl = String.format(PIXEL_TEMPLATE, pixelName) val interceptedUrl = pixelRemovalInterceptor.intercept(FakeChain(pixelUrl)).request.url assertNull(interceptedUrl.queryParameter("atb")) @@ -70,7 +67,7 @@ class PixelParamRemovalInterceptorTest { @Test fun whenSendPixelRedactOSVersion() { - testPixels.filter { it.second == setOf(OS_VERSION) }.map { it.first }.forEach { pixelName -> + testPixels.filter { it.second == PixelParameter.removeOSVersion() }.map { it.first }.forEach { pixelName -> val pixelUrl = String.format(PIXEL_TEMPLATE, pixelName) val interceptedUrl = pixelRemovalInterceptor.intercept(FakeChain(pixelUrl)).request.url assertNotNull(interceptedUrl.queryParameter("atb")) @@ -81,7 +78,7 @@ class PixelParamRemovalInterceptorTest { @Test fun whenSendPixelRedactAtbAndAppAndOSVersion() { - testPixels.filter { it.second.contains(OS_VERSION) && it.second.contains(ATB) && it.second.contains(APP_VERSION) } + testPixels.filter { it.second.containsAll(PixelParameter.removeAll()) } .map { it.first } .forEach { pixelName -> val pixelUrl = String.format(PIXEL_TEMPLATE, pixelName) @@ -95,7 +92,7 @@ class PixelParamRemovalInterceptorTest { companion object { private const val PIXEL_TEMPLATE = "https://improving.duckduckgo.com/t/%s_android_phone?atb=v255-7zu&appVersion=5.74.0&os_version=1.0&test=1" private val testPixels = listOf( - "atb_and_version_redacted" to PixelParameter.removeAll(), + "atb_and_version_and_os_redacted" to PixelParameter.removeAll(), "atb_redacted" to PixelParameter.removeAtb(), "version_redacted" to PixelParameter.removeVersion(), "os_version_redacted" to PixelParameter.removeOSVersion(), diff --git a/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/pixel/PixelParamRemovalPlugin.kt b/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/pixel/PixelParamRemovalPlugin.kt index edd07b1c6638..4a306736ef69 100644 --- a/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/pixel/PixelParamRemovalPlugin.kt +++ b/common/common-utils/src/main/java/com/duckduckgo/common/utils/plugins/pixel/PixelParamRemovalPlugin.kt @@ -37,7 +37,7 @@ interface PixelParamRemovalPlugin { fun removeAll() = setOf(ATB, APP_VERSION, OS_VERSION) fun removeAtb() = setOf(ATB) fun removeVersion() = setOf(APP_VERSION) - fun removeOSVersion() = setOf(APP_VERSION) + fun removeOSVersion() = setOf(OS_VERSION) } } } From 0f9fcd8b84d22338e5446358b7076b616ebb2543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Wed, 14 Feb 2024 15:51:56 +0100 Subject: [PATCH 04/19] Sync: Update Maestro Flows to new Sync screen (#4176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/414730916066338/1206502132931124/f ### Description Update Sync tests so we don’t look for the QR image anymore ### Steps to test this PR Run flow in maestro or locally creating a test account --- .maestro/sync_flows/create_account.yaml | 3 +-- .maestro/sync_flows/delete_server_data.yaml | 2 +- .maestro/sync_flows/recover_account.yaml | 10 +++++++--- .../steps/action_add_bookmarks_and_folders.yaml | 14 ++++++++++++-- .../sync_flows/steps/action_add_new_device.yaml | 3 +-- .../sync_flows/steps/action_recover_account.yaml | 3 +-- 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.maestro/sync_flows/create_account.yaml b/.maestro/sync_flows/create_account.yaml index f0555f55b100..fc84802874bf 100644 --- a/.maestro/sync_flows/create_account.yaml +++ b/.maestro/sync_flows/create_account.yaml @@ -18,5 +18,4 @@ tags: timeout: 5000 - tapOn: "Next" - tapOn: "Done" -- assertVisible: - id: "com.duckduckgo.mobile.android:id/qrCodeImageView" \ No newline at end of file +- assertVisible: "Synced Devices" \ No newline at end of file diff --git a/.maestro/sync_flows/delete_server_data.yaml b/.maestro/sync_flows/delete_server_data.yaml index 84cbf14999e9..88b54a88158b 100644 --- a/.maestro/sync_flows/delete_server_data.yaml +++ b/.maestro/sync_flows/delete_server_data.yaml @@ -14,5 +14,5 @@ tags: direction: DOWN - assertVisible: "Turn Off and Delete Server Data…" - tapOn: "Turn Off and Delete Server Data…" -- tapOn: "Turn Off" +- tapOn: "Delete Data" - assertVisible: "Sync and Back Up This Device" \ No newline at end of file diff --git a/.maestro/sync_flows/recover_account.yaml b/.maestro/sync_flows/recover_account.yaml index 08038f921d3c..ac59aa41648a 100644 --- a/.maestro/sync_flows/recover_account.yaml +++ b/.maestro/sync_flows/recover_account.yaml @@ -10,9 +10,13 @@ tags: clearState: true stopApp: true - runFlow: create_account.yaml -- tapOn: "Show Text Code" -- copyTextFrom: - id: "com.duckduckgo.mobile.android:id/recoveryCode" +- tapOn: "Sync With Another Device" +- runFlow: + when: + visible: "Don't allow" + commands: + - tapOn: "Don't allow" +- tapOn: "Can't Scan? Share Text Code" - tapOn: "Navigate up" - tapOn: "Turn Off Sync & Backup…" - tapOn: "Turn Off" diff --git a/.maestro/sync_flows/steps/action_add_bookmarks_and_folders.yaml b/.maestro/sync_flows/steps/action_add_bookmarks_and_folders.yaml index 70b4d4a99cfc..bd6f44246688 100644 --- a/.maestro/sync_flows/steps/action_add_bookmarks_and_folders.yaml +++ b/.maestro/sync_flows/steps/action_add_bookmarks_and_folders.yaml @@ -42,10 +42,20 @@ appId: com.duckduckgo.mobile.android - tapOn: id: "com.duckduckgo.mobile.android:id/item_container" index: 1 -- tapOn: - id: "com.duckduckgo.mobile.android:id/touch_outside" +- runFlow: + when: + visible: + id: "com.duckduckgo.mobile.android:id/touch_outside" + commands: + - tapOn: + id: "com.duckduckgo.mobile.android:id/touch_outside" # Now, we create a new folder - runFlow: ../../shared/open_bookmarks.yaml +- runFlow: + when: + visible: "Download Missing Icons?" + commands: + - tapOn: "Not Now" - tapOn: "Add Folder" - tapOn: "Title" - inputText: "${output.bookmarks.folders[0]}" diff --git a/.maestro/sync_flows/steps/action_add_new_device.yaml b/.maestro/sync_flows/steps/action_add_new_device.yaml index 19be03e81082..6442bd214761 100644 --- a/.maestro/sync_flows/steps/action_add_new_device.yaml +++ b/.maestro/sync_flows/steps/action_add_new_device.yaml @@ -21,7 +21,6 @@ name: "Sync Critical Path: Devices can be added to an existing account" text: "Sync & Backup" direction: UP - tapOn: "Sync & Backup" -- assertVisible: - id: "com.duckduckgo.mobile.android:id/qrCodeImageView" +- assertVisible: "Synced Devices" - tapOn: "Navigate up" - tapOn: "Navigate up" \ No newline at end of file diff --git a/.maestro/sync_flows/steps/action_recover_account.yaml b/.maestro/sync_flows/steps/action_recover_account.yaml index 65cfd231b307..66572713b68e 100644 --- a/.maestro/sync_flows/steps/action_recover_account.yaml +++ b/.maestro/sync_flows/steps/action_recover_account.yaml @@ -12,5 +12,4 @@ appId: com.duckduckgo.mobile.android timeout: 5000 - tapOn: "Next" - tapOn: "Done" -- assertVisible: - id: "com.duckduckgo.mobile.android:id/qrCodeImageView" \ No newline at end of file +- assertVisible: "Synced Devices" \ No newline at end of file From 6a48a0cf110f7139654f95b6f496081399b9f8b1 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Thu, 15 Feb 2024 11:32:37 +0100 Subject: [PATCH 05/19] Migrate autoconsent to the new feature flags system (#4146) Task/Issue URL: https://app.asana.com/0/1202552961248957/1206497499460618/f ### Description Migrate autoconsent to the new feature flags system in preparation for https://app.asana.com/0/0/1206446781259751/f ### Steps to test this PR _Feature 1_ - [x] Update a previous version of the app to this one - [x] Use database inspector and check values for exceptions and settings persist after the update _Feature 2_ - [x] Do a fresh install of the app - [x] Use database inspector and check values for settings and exceptions are added _ Feature 3_ - [x] Disable autoconsent feature in remote config - [x] Go to settings and enable CPM - [x] Open amazon.es and check cookies aren't managed ### UI changes No UI changes --- .../app/browser/BrowserTabViewModelTest.kt | 4 - .../pageloadpixel/PageLoadedHandlerTest.kt | 4 + .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 15 -- .../app/browser/BrowserTabFragment.kt | 35 +---- .../pageloadpixel/PageLoadedHandler.kt | 3 + .../PageLoadedOfflinePixelSender.kt | 2 + .../pageloadpixel/PageLoadedPixelEntity.kt | 1 + .../java/com/duckduckgo/app/cta/ui/Cta.kt | 31 ---- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 18 --- .../duckduckgo/app/global/db/AppDatabase.kt | 9 +- .../duckduckgo/autoconsent/api/Autoconsent.kt | 5 + autoconsent/autoconsent-impl/build.gradle | 3 + .../impl/AutoconsentFeaturePlugin.kt | 66 --------- .../impl/AutoconsentFeatureTogglesPlugin.kt | 36 ----- .../autoconsent/impl/RealAutoconsent.kt | 25 ++-- .../autoconsent/impl/di/AutoconsentModule.kt | 27 ++-- .../impl/handlers/InitMessageHandlerPlugin.kt | 6 +- .../AutoconsentExceptionsRepository.kt} | 39 ++--- .../AutoconsentExceptionsStore.kt | 34 +++++ .../impl/remoteconfig/AutoconsentFeature.kt | 42 ++++++ .../AutoconsentFeatureModels.kt} | 21 ++- .../AutoconsentFeatureSettingsRepository.kt | 55 +++++++ .../remoteconfig/AutoconsentSettingsStore.kt | 47 ++++++ .../impl/AutoconsentFeaturePluginTest.kt | 107 -------------- .../AutoconsentFeatureTogglesPluginTest.kt | 139 ------------------ .../autoconsent/impl/RealAutoconsentTest.kt | 24 +-- .../handlers/InitMessageHandlerPluginTest.kt | 9 +- ...ealAutoconsentExceptionsRepositoryTest.kt} | 37 +++-- ...utoconsentFeatureSettingsRepositoryTest.kt | 91 ++++++++++++ .../ui/AutoconsentSettingsViewModelTest.kt | 4 + .../autoconsent/store/AutoconsentDao.kt | 12 ++ .../AutoconsentFeatureToggleRepository.kt | 34 ----- .../store/AutoconsentFeatureToggleStore.kt | 71 --------- .../store/AutoconsentSettingsDataStore.kt | 15 +- .../store/AutoconsentSettingsRepository.kt | 3 +- 35 files changed, 421 insertions(+), 653 deletions(-) delete mode 100644 autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeaturePlugin.kt delete mode 100644 autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeatureTogglesPlugin.kt rename autoconsent/{autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentRepository.kt => autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsRepository.kt} (53%) create mode 100644 autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsStore.kt create mode 100644 autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeature.kt rename autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/{AutoconsentFeature.kt => remoteconfig/AutoconsentFeatureModels.kt} (60%) create mode 100644 autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeatureSettingsRepository.kt create mode 100644 autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentSettingsStore.kt delete mode 100644 autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/AutoconsentFeaturePluginTest.kt delete mode 100644 autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/AutoconsentFeatureTogglesPluginTest.kt rename autoconsent/{autoconsent-store/src/test/java/com/duckduckgo/autoconsent/store/RealAutoconsentRepositoryTest.kt => autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentExceptionsRepositoryTest.kt} (60%) create mode 100644 autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentFeatureSettingsRepositoryTest.kt delete mode 100644 autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentFeatureToggleRepository.kt delete mode 100644 autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentFeatureToggleStore.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index ba1d275130ff..198cb1b02490 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -130,7 +130,6 @@ import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonito import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.InstantSchedulersRule -import com.duckduckgo.common.ui.store.AppTheme import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.device.DeviceInfo import com.duckduckgo.downloads.api.DownloadStateListener @@ -395,8 +394,6 @@ class BrowserTabViewModelTest { private val favoriteListFlow = Channel>() - private val mockAppTheme: AppTheme = mock() - private val autofillCapabilityChecker: FakeCapabilityChecker = FakeCapabilityChecker(enabled = false) private val autofillFireproofDialogSuppressor: AutofillFireproofDialogSuppressor = mock() @@ -465,7 +462,6 @@ class BrowserTabViewModelTest { tabRepository = mockTabRepository, dispatchers = coroutineRule.testDispatcherProvider, duckDuckGoUrlDetector = DuckDuckGoUrlDetectorImpl(), - appTheme = mockAppTheme, surveyRepository = mockSurveyRepository, ) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedHandlerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedHandlerTest.kt index 2caed318ba4d..d7b98e5079a0 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedHandlerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedHandlerTest.kt @@ -1,6 +1,7 @@ package com.duckduckgo.app.browser.pageloadpixel import com.duckduckgo.app.pixels.remoteconfig.OptimizeTrackerEvaluationRCWrapper +import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.browser.api.WebViewVersionProvider import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.device.DeviceInfo @@ -26,6 +27,7 @@ class PageLoadedHandlerTest { private val deviceInfo: DeviceInfo = mock() private val webViewVersionProvider: WebViewVersionProvider = mock() private val pageLoadedPixelDao: PageLoadedPixelDao = mock() + private val autoconsent: Autoconsent = mock() private val optimizeTrackerEvaluationRCWrapper = object : OptimizeTrackerEvaluationRCWrapper { override val enabled: Boolean get() = true @@ -38,12 +40,14 @@ class PageLoadedHandlerTest { TestScope(), coroutinesTestRule.testDispatcherProvider, optimizeTrackerEvaluationRCWrapper, + autoconsent, ) @Before fun before() { whenever(webViewVersionProvider.getMajorVersion()).thenReturn("1") whenever(deviceInfo.appVersion).thenReturn("1") + whenever(autoconsent.isAutoconsentEnabled()).thenReturn(true) } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 51e2637ef656..44a32c96eb13 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -51,7 +51,6 @@ import com.duckduckgo.app.trackerdetection.model.TrackingEvent import com.duckduckgo.app.widget.ui.WidgetCapabilities import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.InstantSchedulersRule -import com.duckduckgo.common.ui.store.AppTheme import java.util.concurrent.TimeUnit import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.drop @@ -111,9 +110,6 @@ class CtaViewModelTest { @Mock private lateinit var mockTabRepository: TabRepository - @Mock - private lateinit var mockAppTheme: AppTheme - @Mock private lateinit var mockSurveyRepository: SurveyRepository @@ -154,7 +150,6 @@ class CtaViewModelTest { tabRepository = mockTabRepository, dispatchers = coroutineRule.testDispatcherProvider, duckDuckGoUrlDetector = DuckDuckGoUrlDetectorImpl(), - appTheme = mockAppTheme, surveyRepository = mockSurveyRepository, ) } @@ -466,16 +461,6 @@ class CtaViewModelTest { assertTrue(value is DaxDialogCta.DaxNoSerpCta) } - @Test - fun whenRefreshCtaWhileBrowsingAndAutoconsentPresentThenReturnOtherCta() = runTest { - givenDaxOnboardingActive() - testee.enableAutoconsentCta() - val site = site(url = "http://www.wikipedia.com") - val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) - - assertTrue(value is DaxDialogCta.DaxAutoconsentCta) - } - @Test fun whenRefreshCtaOnHomeTabThenValueReturnedIsNotDaxDialogCtaType() = runTest { givenDaxOnboardingActive() diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index a8088590d5ec..2343976c50e7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -592,13 +592,7 @@ class BrowserTabFragment : } private val autoconsentCallback = object : AutoconsentCallback { - override fun onFirstPopUpHandled() { - // Remove comment to promote feature - // ctaViewModel.enableAutoconsentCta() - // launch { - // viewModel.refreshCta() - // } - } + override fun onFirstPopUpHandled() { } override fun onPopUpHandled(isCosmetic: Boolean) { launch { @@ -3674,8 +3668,7 @@ class BrowserTabFragment : val configuration = lastSeenCtaViewState?.cta if (configuration is DaxDialogCta) { activity?.let { activity -> - val listener = if (configuration is DaxAutoconsentCta) daxAutoconsentListener else daxListener - configuration.createCta(activity, listener).apply { + configuration.createCta(activity, daxListener).apply { showDialogHidingPrevious(this, DAX_DIALOG_DIALOG_TAG) } } @@ -3686,35 +3679,13 @@ class BrowserTabFragment : hideHomeCta() hideDaxCta() activity?.let { activity -> - val listener = if (configuration is DaxAutoconsentCta) daxAutoconsentListener else daxListener - configuration.createCta(activity, listener).apply { + configuration.createCta(activity, daxListener).apply { showDialogIfNotExist(this, DAX_DIALOG_DIALOG_TAG) } viewModel.onCtaShown() } } - private val daxAutoconsentListener = object : DaxDialogListener { - override fun onDaxDialogDismiss() { - autoconsent.firstPopUpHandled() - viewModel.onDaxDialogDismissed() - } - - override fun onDaxDialogHideClick() { - viewModel.onUserHideDaxDialog() - } - - override fun onDaxDialogPrimaryCtaClick() { - webView?.let { autoconsent.setAutoconsentOptOut(it) } - viewModel.onUserClickCtaOkButton() - } - - override fun onDaxDialogSecondaryCtaClick() { - autoconsent.setAutoconsentOptIn() - viewModel.onUserClickCtaSecondaryButton() - } - } - private val daxListener = object : DaxDialogListener { override fun onDaxDialogDismiss() { viewModel.onDaxDialogDismissed() diff --git a/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedHandler.kt b/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedHandler.kt index 3b69191651a8..4478dd7cc8ea 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedHandler.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedHandler.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.browser.pageloadpixel import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.pixels.remoteconfig.OptimizeTrackerEvaluationRCWrapper +import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.browser.api.WebViewVersionProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.UriString @@ -51,6 +52,7 @@ class RealPageLoadedHandler @Inject constructor( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, private val optimizeTrackerEvaluationRCWrapper: OptimizeTrackerEvaluationRCWrapper, + private val autoconsent: Autoconsent, ) : PageLoadedHandler { override operator fun invoke(url: String, start: Long, end: Long) { @@ -62,6 +64,7 @@ class RealPageLoadedHandler @Inject constructor( webviewVersion = webViewVersionProvider.getMajorVersion(), appVersion = deviceInfo.appVersion, trackerOptimizationEnabled = optimizeTrackerEvaluationRCWrapper.enabled, + cpmEnabled = autoconsent.isAutoconsentEnabled(), ), ) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedOfflinePixelSender.kt b/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedOfflinePixelSender.kt index 0a6bfcf35d29..d6c0047c6b06 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedOfflinePixelSender.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedOfflinePixelSender.kt @@ -29,6 +29,7 @@ import timber.log.Timber private const val ELAPSED_TIME = "elapsed_time" private const val WEBVIEW_VERSION = "webview_version" private const val TRACKER_OPTIMIZATION_ENABLED = "tracker_optimization_enabled" +private const val CPM_ENABLED = "cpm_enabled" // This is used to ensure the app version we send is the one from the moment the page was loaded, and not then the pixel is fired later on private const val APP_VERSION = "app_version_when_page_loaded" @@ -49,6 +50,7 @@ class PageLoadedOfflinePixelSender @Inject constructor( ELAPSED_TIME to it.elapsedTime.toString(), WEBVIEW_VERSION to it.webviewVersion, TRACKER_OPTIMIZATION_ENABLED to it.trackerOptimizationEnabled.toString(), + CPM_ENABLED to it.cpmEnabled.toString(), ) val pixel = pixelSender.sendPixel( diff --git a/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedPixelEntity.kt b/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedPixelEntity.kt index 22ac4f395c24..29e83142907c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedPixelEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/pageloadpixel/PageLoadedPixelEntity.kt @@ -26,4 +26,5 @@ class PageLoadedPixelEntity( val elapsedTime: Long, val webviewVersion: String, val trackerOptimizationEnabled: Boolean, + val cpmEnabled: Boolean, ) diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 6904f459835c..4daf955aa3e8 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -37,9 +37,7 @@ import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelValues.DAX_FIRE_DIALOG_CTA import com.duckduckgo.app.trackerdetection.model.Entity -import com.duckduckgo.common.ui.store.AppTheme import com.duckduckgo.common.ui.view.DaxDialogListener -import com.duckduckgo.common.ui.view.LottieDaxDialog import com.duckduckgo.common.ui.view.TypeAnimationTextView import com.duckduckgo.common.ui.view.TypewriterDaxDialog import com.duckduckgo.common.ui.view.gone @@ -239,35 +237,6 @@ sealed class DaxDialogCta( } } - class DaxAutoconsentCta( - override val onboardingStore: OnboardingStore, - override val appInstallStore: AppInstallStore, - private val appTheme: AppTheme, - ) : DaxDialogCta( - CtaId.DAX_DIALOG_AUTOCONSENT, - AppPixelName.ONBOARDING_DAX_CTA_SHOWN, - AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, - null, - Pixel.PixelValues.DAX_AUTOCONSENT_CTA, - onboardingStore, - appInstallStore, - ) { - override fun createCta(context: Context, daxDialogListener: DaxDialogListener): DialogFragment { - val lottieRes = if (appTheme.isLightModeEnabled()) R.raw.cookie_banner_light else R.raw.cookie_banner_dark - val dialog = LottieDaxDialog.newInstance( - titleText = context.getString(R.string.autoconsentDialogTitle), - descriptionText = context.getString(R.string.autoconsentDialogDescription), - lottieRes = lottieRes, - primaryButtonText = context.getString(R.string.autoconsentPrimaryCta), - secondaryButtonText = context.getString(R.string.autoconsentSecondaryCta), - hideButtonText = context.getString(R.string.daxDialogHideButton), - showHideButton = false, - ) - dialog.setDaxDialogListener(daxDialogListener) - return dialog - } - } - companion object { private const val MAX_TRACKERS_SHOWS = 2 const val SERP = "duckduckgo" diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index afe2e8b385c7..80ca3e126b20 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -38,11 +38,9 @@ import com.duckduckgo.app.survey.api.SurveyRepository import com.duckduckgo.app.survey.model.Survey import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.widget.ui.WidgetCapabilities -import com.duckduckgo.common.ui.store.AppTheme import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import dagger.SingleInstanceIn -import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -64,11 +62,9 @@ class CtaViewModel @Inject constructor( private val tabRepository: TabRepository, private val dispatchers: DispatcherProvider, private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector, - private val appTheme: AppTheme, private val surveyRepository: SurveyRepository, ) { val surveyLiveData: LiveData = surveyRepository.getScheduledLiveSurvey() - var canShowAutoconsentCta: AtomicBoolean = AtomicBoolean(false) @ExperimentalCoroutinesApi @VisibleForTesting @@ -261,14 +257,6 @@ class CtaViewModel @Inject constructor( return null } - val oldAutoconsentValue = canShowAutoconsentCta.get() - canShowAutoconsentCta.set(false) - - // Autoconsent - if (oldAutoconsentValue && !daxDialogAutoconsentShown()) { - return DaxDialogCta.DaxAutoconsentCta(onboardingStore, appInstallStore, appTheme) - } - if (!canShowDaxDialogCta()) return null // Trackers blocked @@ -297,14 +285,8 @@ class CtaViewModel @Inject constructor( } } - fun enableAutoconsentCta() { - canShowAutoconsentCta.set(true) - } - private fun daxDialogIntroShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_INTRO) - private fun daxDialogAutoconsentShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_AUTOCONSENT) - private fun daxDialogEndShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_END) private fun daxDialogSerpShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_SERP) diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 28e20e7608a3..9d30a84caca1 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -68,7 +68,7 @@ import com.duckduckgo.savedsites.store.SavedSitesRelationsDao @Database( exportSchema = true, - version = 51, + version = 52, entities = [ TdsTracker::class, TdsEntity::class, @@ -635,6 +635,12 @@ class MigrationsProvider(val context: Context, val settingsDataStore: SettingsDa } } + private val MIGRATION_51_TO_52: Migration = object : Migration(51, 52) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE `page_loaded_pixel_entity` ADD COLUMN `cpmEnabled` INTEGER NOT NULL DEFAULT 0") + } + } + val BOOKMARKS_DB_ON_CREATE = object : RoomDatabase.Callback() { override fun onCreate(database: SupportSQLiteDatabase) { database.execSQL( @@ -711,6 +717,7 @@ class MigrationsProvider(val context: Context, val settingsDataStore: SettingsDa MIGRATION_48_TO_49, MIGRATION_49_TO_50, MIGRATION_50_TO_51, + MIGRATION_51_TO_52, ) @Deprecated( diff --git a/autoconsent/autoconsent-api/src/main/java/com/duckduckgo/autoconsent/api/Autoconsent.kt b/autoconsent/autoconsent-api/src/main/java/com/duckduckgo/autoconsent/api/Autoconsent.kt index 854f92e37c9a..386179288a81 100644 --- a/autoconsent/autoconsent-api/src/main/java/com/duckduckgo/autoconsent/api/Autoconsent.kt +++ b/autoconsent/autoconsent-api/src/main/java/com/duckduckgo/autoconsent/api/Autoconsent.kt @@ -41,6 +41,11 @@ interface Autoconsent { */ fun isSettingEnabled(): Boolean + /** + * @return `true` if autoconsent is enabled in remote config and enabled by the user, `false` otherwise. + */ + fun isAutoconsentEnabled(): Boolean + /** * This method sends and opt out message to autoconsent on the given [WebView] instance to set the opt out mode. */ diff --git a/autoconsent/autoconsent-impl/build.gradle b/autoconsent/autoconsent-impl/build.gradle index d0a803d47b20..b6a538143c77 100644 --- a/autoconsent/autoconsent-impl/build.gradle +++ b/autoconsent/autoconsent-impl/build.gradle @@ -61,6 +61,9 @@ dependencies { implementation AndroidX.lifecycle.runtime.ktx implementation AndroidX.room.runtime implementation AndroidX.constraintLayout + implementation Square.moshi + implementation "com.squareup.moshi:moshi-kotlin:_" + implementation "com.squareup.moshi:moshi-adapters:_" // Testing dependencies testImplementation project(path: ':common-test') diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeaturePlugin.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeaturePlugin.kt deleted file mode 100644 index 8fe2906330ad..000000000000 --- a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeaturePlugin.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2022 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.autoconsent.impl - -import com.duckduckgo.autoconsent.api.AutoconsentFeatureName -import com.duckduckgo.autoconsent.store.AutoconsentExceptionEntity -import com.duckduckgo.autoconsent.store.AutoconsentFeatureToggleRepository -import com.duckduckgo.autoconsent.store.AutoconsentFeatureToggles -import com.duckduckgo.autoconsent.store.AutoconsentRepository -import com.duckduckgo.autoconsent.store.DisabledCmpsEntity -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.privacy.config.api.PrivacyFeaturePlugin -import com.squareup.anvil.annotations.ContributesMultibinding -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import javax.inject.Inject - -@ContributesMultibinding(AppScope::class) -class AutoconsentFeaturePlugin @Inject constructor( - private val autoconsentRepository: AutoconsentRepository, - private val autoconsentFeatureToggleRepository: AutoconsentFeatureToggleRepository, -) : PrivacyFeaturePlugin { - - override fun store(featureName: String, jsonString: String): Boolean { - val autoconsentFeatureName = autoconsentFeatureValueOf(featureName) ?: return false - if (autoconsentFeatureName.value == this.featureName) { - val moshi = Moshi.Builder().build() - val jsonAdapter: JsonAdapter = - moshi.adapter(AutoconsentFeature::class.java) - - val autoconsentFeature: AutoconsentFeature? = jsonAdapter.fromJson(jsonString) - - val disabledCmps = autoconsentFeature?.settings?.disabledCMPs?.map { - DisabledCmpsEntity(it) - }.orEmpty() - - val exceptions = autoconsentFeature?.exceptions?.map { - AutoconsentExceptionEntity(domain = it.domain, reason = it.reason.orEmpty()) - }.orEmpty() - - autoconsentRepository.updateAll(exceptions, disabledCmps) - val isEnabled = autoconsentFeature?.state == "enabled" - autoconsentFeatureToggleRepository.insert( - AutoconsentFeatureToggles(autoconsentFeatureName, isEnabled, autoconsentFeature?.minSupportedVersion), - ) - return true - } - return false - } - - override val featureName: String = AutoconsentFeatureName.Autoconsent.value -} diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeatureTogglesPlugin.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeatureTogglesPlugin.kt deleted file mode 100644 index 55bc06594ca0..000000000000 --- a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeatureTogglesPlugin.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2022 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.autoconsent.impl - -import com.duckduckgo.appbuildconfig.api.AppBuildConfig -import com.duckduckgo.autoconsent.store.AutoconsentFeatureToggleRepository -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.feature.toggles.api.FeatureTogglesPlugin -import com.squareup.anvil.annotations.ContributesMultibinding -import javax.inject.Inject - -@ContributesMultibinding(AppScope::class) -class AutoconsentFeatureTogglesPlugin @Inject constructor( - private val autoconsentFeatureToggleRepository: AutoconsentFeatureToggleRepository, - private val appBuildConfig: AppBuildConfig, -) : FeatureTogglesPlugin { - override fun isEnabled(featureName: String, defaultValue: Boolean): Boolean? { - val autoconsentFeature = autoconsentFeatureValueOf(featureName) ?: return null - return autoconsentFeatureToggleRepository.get(autoconsentFeature, defaultValue) && - appBuildConfig.versionCode >= autoconsentFeatureToggleRepository.getMinSupportedVersion(autoconsentFeature) - } -} diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/RealAutoconsent.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/RealAutoconsent.kt index 96c3a3d1aabd..6e805a0119f5 100644 --- a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/RealAutoconsent.kt +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/RealAutoconsent.kt @@ -20,15 +20,14 @@ import android.webkit.WebView import com.duckduckgo.app.privacy.db.UserAllowListRepository import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autoconsent.api.AutoconsentCallback -import com.duckduckgo.autoconsent.api.AutoconsentFeatureName import com.duckduckgo.autoconsent.impl.AutoconsentInterface.Companion.AUTOCONSENT_INTERFACE import com.duckduckgo.autoconsent.impl.handlers.ReplyHandler -import com.duckduckgo.autoconsent.store.AutoconsentRepository +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentExceptionsRepository +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeature import com.duckduckgo.autoconsent.store.AutoconsentSettingsRepository import com.duckduckgo.common.utils.UriString import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.privacy.config.api.UnprotectedTemporary import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject @@ -37,8 +36,8 @@ import javax.inject.Inject class RealAutoconsent @Inject constructor( private val messageHandlerPlugins: PluginPoint, private val settingsRepository: AutoconsentSettingsRepository, - private val autoconsentRepository: AutoconsentRepository, - private val featureToggle: FeatureToggle, + private val autoconsentExceptionsRepository: AutoconsentExceptionsRepository, + private val autoconsent: AutoconsentFeature, private val userAllowlistRepository: UserAllowListRepository, private val unprotectedTemporary: UnprotectedTemporary, ) : Autoconsent { @@ -46,7 +45,7 @@ class RealAutoconsent @Inject constructor( private lateinit var autoconsentJs: String override fun injectAutoconsent(webView: WebView, url: String) { - if (canBeInjected() && !urlInUserAllowList(url) && !isAnException(url)) { + if (isAutoconsentEnabled() && !urlInUserAllowList(url) && !isAnException(url)) { webView.evaluateJavascript("javascript:${getFunctionsJS()}", null) } } @@ -66,6 +65,10 @@ class RealAutoconsent @Inject constructor( return settingsRepository.userSetting } + override fun isAutoconsentEnabled(): Boolean { + return isEnabled() && isSettingEnabled() + } + override fun setAutoconsentOptOut(webView: WebView) { settingsRepository.userSetting = true webView.evaluateJavascript("javascript:${ReplyHandler.constructReply("""{ "type": "optOut" }""")}", null) @@ -88,7 +91,7 @@ class RealAutoconsent @Inject constructor( } private fun isEnabled(): Boolean { - return featureToggle.isFeatureEnabled(AutoconsentFeatureName.Autoconsent.value) + return autoconsent.self().isEnabled() } private fun isAnException(url: String): Boolean { @@ -96,13 +99,7 @@ class RealAutoconsent @Inject constructor( } private fun matches(url: String): Boolean { - return autoconsentRepository.exceptions.any { UriString.sameOrSubdomain(url, it.domain) } - } - - private fun canBeInjected(): Boolean { - // Remove comment to promote feature - // return isEnabled() && (settingsRepository.userSetting || !settingsRepository.firstPopupHandled) - return isEnabled() && settingsRepository.userSetting + return autoconsentExceptionsRepository.exceptions.any { UriString.sameOrSubdomain(url, it.domain) } } private fun getFunctionsJS(): String { diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/di/AutoconsentModule.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/di/AutoconsentModule.kt index 35a1baf39c18..aaa71c099899 100644 --- a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/di/AutoconsentModule.kt +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/di/AutoconsentModule.kt @@ -19,11 +19,13 @@ package com.duckduckgo.autoconsent.impl.di import android.content.Context import androidx.room.Room import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentExceptionsRepository +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeature +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeatureSettingsRepository +import com.duckduckgo.autoconsent.impl.remoteconfig.RealAutoconsentExceptionsRepository +import com.duckduckgo.autoconsent.impl.remoteconfig.RealAutoconsentFeatureSettingsRepository import com.duckduckgo.autoconsent.store.AutoconsentDatabase -import com.duckduckgo.autoconsent.store.AutoconsentFeatureToggleRepository -import com.duckduckgo.autoconsent.store.AutoconsentRepository import com.duckduckgo.autoconsent.store.AutoconsentSettingsRepository -import com.duckduckgo.autoconsent.store.RealAutoconsentRepository import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesTo @@ -37,8 +39,9 @@ import kotlinx.coroutines.CoroutineScope object AutoconsentModule { @Provides - fun provideAutoconsentSettingsRepository(context: Context): AutoconsentSettingsRepository { - return AutoconsentSettingsRepository.create(context) + @SingleInstanceIn(AppScope::class) + fun provideAutoconsentSettingsRepository(context: Context, autoconsentFeature: AutoconsentFeature): AutoconsentSettingsRepository { + return AutoconsentSettingsRepository.create(context, autoconsentFeature.onByDefault().isEnabled()) } @Provides @@ -51,17 +54,21 @@ object AutoconsentModule { @SingleInstanceIn(AppScope::class) @Provides - fun provideAutoconsentRepository( + fun provideAutoconsentExceptionsRepository( database: AutoconsentDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, - ): AutoconsentRepository { - return RealAutoconsentRepository(database, appCoroutineScope, dispatcherProvider) + ): AutoconsentExceptionsRepository { + return RealAutoconsentExceptionsRepository(appCoroutineScope, dispatcherProvider, database) } @SingleInstanceIn(AppScope::class) @Provides - fun provideAutoconsentFeatureToggleRepository(context: Context): AutoconsentFeatureToggleRepository { - return AutoconsentFeatureToggleRepository.create(context) + fun provideAutoconsentFeatureSettingsRepository( + database: AutoconsentDatabase, + @AppCoroutineScope appCoroutineScope: CoroutineScope, + dispatcherProvider: DispatcherProvider, + ): AutoconsentFeatureSettingsRepository { + return RealAutoconsentFeatureSettingsRepository(appCoroutineScope, dispatcherProvider, database) } } diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/handlers/InitMessageHandlerPlugin.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/handlers/InitMessageHandlerPlugin.kt index e19c9e3a1f20..5ae3a236b6b9 100644 --- a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/handlers/InitMessageHandlerPlugin.kt +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/handlers/InitMessageHandlerPlugin.kt @@ -23,7 +23,7 @@ import com.duckduckgo.autoconsent.api.AutoconsentCallback import com.duckduckgo.autoconsent.impl.JsReader import com.duckduckgo.autoconsent.impl.MessageHandlerPlugin import com.duckduckgo.autoconsent.impl.adapters.JSONObjectAdapter -import com.duckduckgo.autoconsent.store.AutoconsentRepository +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeatureSettingsRepository import com.duckduckgo.autoconsent.store.AutoconsentSettingsRepository import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.isHttp @@ -43,7 +43,7 @@ class InitMessageHandlerPlugin @Inject constructor( @AppCoroutineScope val appCoroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, private val settingsRepository: AutoconsentSettingsRepository, - private val repository: AutoconsentRepository, + private val autoconsentFeatureSettingsRepository: AutoconsentFeatureSettingsRepository, ) : MessageHandlerPlugin { private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build() @@ -71,7 +71,7 @@ class InitMessageHandlerPlugin @Inject constructor( // Reset site autoconsentCallback.onResultReceived(consentManaged = false, optOutFailed = false, selfTestFailed = false, isCosmetic = false) - val disabledCmps = repository.disabledCmps.map { it.name } + val disabledCmps = autoconsentFeatureSettingsRepository.disabledCMPs val autoAction = getAutoAction() val enablePreHide = settingsRepository.userSetting val detectRetries = 20 diff --git a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentRepository.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsRepository.kt similarity index 53% rename from autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentRepository.kt rename to autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsRepository.kt index 7e9410adfd39..53f0f09fd7b2 100644 --- a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentRepository.kt +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,49 +14,42 @@ * limitations under the License. */ -package com.duckduckgo.autoconsent.store +package com.duckduckgo.autoconsent.impl.remoteconfig +import com.duckduckgo.autoconsent.store.AutoconsentDatabase +import com.duckduckgo.autoconsent.store.AutoconsentExceptionEntity import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.feature.toggles.api.FeatureExceptions.FeatureException import java.util.concurrent.CopyOnWriteArrayList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -interface AutoconsentRepository { - fun updateAll(exceptions: List, disabledCmps: List) - val exceptions: List - val disabledCmps: List +interface AutoconsentExceptionsRepository { + fun insertAllExceptions(exceptions: List) + val exceptions: CopyOnWriteArrayList } -class RealAutoconsentRepository( - val database: AutoconsentDatabase, +class RealAutoconsentExceptionsRepository( coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, -) : AutoconsentRepository { - - private val autoconsentDao: AutoconsentDao = database.autoconsentDao() + val database: AutoconsentDatabase, +) : AutoconsentExceptionsRepository { + private val dao = database.autoconsentDao() override val exceptions = CopyOnWriteArrayList() - override val disabledCmps = CopyOnWriteArrayList() init { - coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() - } + coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } } - override fun updateAll(exceptions: List, disabledCmps: List) { - autoconsentDao.updateAll(exceptions, disabledCmps) + override fun insertAllExceptions(exceptions: List) { + dao.updateAllExceptions(exceptions.map { AutoconsentExceptionEntity(domain = it.domain, reason = it.reason ?: "") }) loadToMemory() } private fun loadToMemory() { exceptions.clear() - autoconsentDao.getExceptions().map { - exceptions.add(it.toFeatureException()) - } - - disabledCmps.clear() - disabledCmps.addAll(autoconsentDao.getDisabledCmps()) + val exceptionsEntityList = dao.getExceptions() + exceptions.addAll(exceptionsEntityList.map { FeatureException(domain = it.domain, reason = it.reason) }) } } diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsStore.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsStore.kt new file mode 100644 index 000000000000..818c25d44048 --- /dev/null +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsStore.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 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.autoconsent.impl.remoteconfig + +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.FeatureExceptions +import com.duckduckgo.feature.toggles.api.FeatureExceptions.FeatureException +import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +@RemoteFeatureStoreNamed(AutoconsentFeature::class) +class AutoconsentExceptionsStore @Inject constructor( + val autoconsentExceptionsRepository: AutoconsentExceptionsRepository, +) : FeatureExceptions.Store { + override fun insertAll(exception: List) { + autoconsentExceptionsRepository.insertAllExceptions(exception) + } +} diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeature.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeature.kt new file mode 100644 index 000000000000..5119adb78c69 --- /dev/null +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeature.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 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.autoconsent.impl.remoteconfig + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle + +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "autoconsent", + settingsStore = AutoconsentFeatureSettingsStore::class, + exceptionsStore = AutoconsentExceptionsStore::class, +) +/** + * This is the class that represents the voiceSearch feature flags + */ +interface AutoconsentFeature { + /** + * @return `true` when the remote config has the global "voiceSearch" feature flag enabled + * If the remote feature is not present defaults to `true` + */ + @Toggle.DefaultValue(true) + fun self(): Toggle + + @Toggle.DefaultValue(false) + fun onByDefault(): Toggle +} diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeature.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeatureModels.kt similarity index 60% rename from autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeature.kt rename to autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeatureModels.kt index f4dfe25a3e2e..da12df1d2109 100644 --- a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/AutoconsentFeature.kt +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeatureModels.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,17 +14,14 @@ * limitations under the License. */ -package com.duckduckgo.autoconsent.impl +package com.duckduckgo.autoconsent.impl.remoteconfig -import com.duckduckgo.feature.toggles.api.FeatureExceptions.FeatureException +import com.squareup.moshi.Json -data class AutoconsentFeature( - val state: String, - val minSupportedVersion: Int?, - val exceptions: List, - val settings: Settings, -) +class AutoconsentFeatureModels { -data class Settings( - val disabledCMPs: List, -) + data class AutoconsentSettings( + @field:Json(name = "disabledCMPs") + val disabledCMPs: List, + ) +} diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeatureSettingsRepository.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeatureSettingsRepository.kt new file mode 100644 index 000000000000..b4d69bd8d423 --- /dev/null +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentFeatureSettingsRepository.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 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.autoconsent.impl.remoteconfig + +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeatureModels.AutoconsentSettings +import com.duckduckgo.autoconsent.store.AutoconsentDatabase +import com.duckduckgo.autoconsent.store.DisabledCmpsEntity +import com.duckduckgo.common.utils.DispatcherProvider +import java.util.concurrent.CopyOnWriteArrayList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +interface AutoconsentFeatureSettingsRepository { + fun updateAllSettings(settings: AutoconsentSettings) + val disabledCMPs: CopyOnWriteArrayList +} + +class RealAutoconsentFeatureSettingsRepository( + coroutineScope: CoroutineScope, + dispatcherProvider: DispatcherProvider, + val database: AutoconsentDatabase, +) : AutoconsentFeatureSettingsRepository { + + override val disabledCMPs = CopyOnWriteArrayList() + private val dao = database.autoconsentDao() + + init { + coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + } + + override fun updateAllSettings(settings: AutoconsentSettings) { + dao.updateAllDisabledCMPs(settings.disabledCMPs.map { DisabledCmpsEntity(it) }) + loadToMemory() + } + + private fun loadToMemory() { + disabledCMPs.clear() + val disabledCMPsEntityList = dao.getDisabledCmps() + disabledCMPs.addAll(disabledCMPsEntityList.map { it.name }) + } +} diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentSettingsStore.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentSettingsStore.kt new file mode 100644 index 000000000000..f199226a57d1 --- /dev/null +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentSettingsStore.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 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.autoconsent.impl.remoteconfig + +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeatureModels.AutoconsentSettings +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.FeatureSettings +import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +@RemoteFeatureStoreNamed(AutoconsentFeature::class) +class AutoconsentFeatureSettingsStore @Inject constructor( + private val autoconsentFeatureSettingsRepository: AutoconsentFeatureSettingsRepository, +) : FeatureSettings.Store { + + private val jsonAdapter by lazy { buildJsonAdapter() } + + override fun store(jsonString: String) { + jsonAdapter.fromJson(jsonString)?.let { + autoconsentFeatureSettingsRepository.updateAllSettings(it) + } + } + + private fun buildJsonAdapter(): JsonAdapter { + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + return moshi.adapter(AutoconsentSettings::class.java) + } +} diff --git a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/AutoconsentFeaturePluginTest.kt b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/AutoconsentFeaturePluginTest.kt deleted file mode 100644 index d7c65f8404ef..000000000000 --- a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/AutoconsentFeaturePluginTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2022 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.autoconsent.impl - -import com.duckduckgo.autoconsent.api.AutoconsentFeatureName -import com.duckduckgo.autoconsent.store.AutoconsentExceptionEntity -import com.duckduckgo.autoconsent.store.AutoconsentFeatureToggleRepository -import com.duckduckgo.autoconsent.store.AutoconsentFeatureToggles -import com.duckduckgo.autoconsent.store.AutoconsentRepository -import com.duckduckgo.autoconsent.store.DisabledCmpsEntity -import com.duckduckgo.common.test.FileUtilities -import org.junit.Assert.* -import org.junit.Before -import org.junit.Test -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify - -class AutoconsentFeaturePluginTest { - - lateinit var plugin: AutoconsentFeaturePlugin - - private val mockAutoconsentRepository: AutoconsentRepository = mock() - private val mockAutoconsentFeatureToggleRepository: AutoconsentFeatureToggleRepository = mock() - - @Before - fun before() { - plugin = AutoconsentFeaturePlugin(mockAutoconsentRepository, mockAutoconsentFeatureToggleRepository) - } - - @Test - fun whenFeatureNameDoesNotMatchAutoconsentFeatureNameValuesThenReturnFalse() { - AutoconsentFeatureName.values().filter { it != FEATURE_NAME }.forEach { - assertFalse(plugin.store(it.value, EMPTY_JSON_STRING)) - } - } - - @Test - fun whenFeatureNameMatchesAutoconsentThenReturnTrue() { - assertTrue(plugin.store(FEATURE_NAME_VALUE, EMPTY_JSON_STRING)) - } - - @Test - fun whenFeatureNameMatchesAutoconsentAndIsEnabledThenStoreFeatureEnabled() { - val jsonString = FileUtilities.loadText(javaClass.classLoader!!, "json/autoconsent.json") - - plugin.store(FEATURE_NAME_VALUE, jsonString) - - verify(mockAutoconsentFeatureToggleRepository).insert(AutoconsentFeatureToggles(FEATURE_NAME, true, null)) - } - - @Test - fun whenFeatureNameMatchesAutoconsentAndIsNotEnabledThenStoreFeatureDisabled() { - val jsonString = FileUtilities.loadText(javaClass.classLoader!!, "json/autoconsent_disabled.json") - - plugin.store(FEATURE_NAME_VALUE, jsonString) - - verify(mockAutoconsentFeatureToggleRepository).insert(AutoconsentFeatureToggles(FEATURE_NAME, false, null)) - } - - @Test - fun whenFeatureNameMatchesAutoconsentThenUpdateAllExistingLists() { - val jsonString = FileUtilities.loadText(javaClass.classLoader!!, "json/autoconsent.json") - - plugin.store(FEATURE_NAME_VALUE, jsonString) - - val exceptions = argumentCaptor>() - val disabledCmps = argumentCaptor>() - - verify(mockAutoconsentRepository).updateAll(exceptions = exceptions.capture(), disabledCmps = disabledCmps.capture()) - - val exceptionsEntity = exceptions.firstValue - assertEquals(2, exceptionsEntity.size) - - val disabledCmpsEntity = disabledCmps.firstValue - assertEquals(1, disabledCmpsEntity.size) - } - - @Test - fun whenFeatureNameMatchesAdClickAttributionAndHasMinSupportedVersionThenStoreMinSupportedVersion() { - val jsonString = FileUtilities.loadText(javaClass.classLoader!!, "json/autoconsent_min_supported_version.json") - - plugin.store(FEATURE_NAME_VALUE, jsonString) - - verify(mockAutoconsentFeatureToggleRepository).insert(AutoconsentFeatureToggles(FEATURE_NAME, true, 1234)) - } - - companion object { - private val FEATURE_NAME = AutoconsentFeatureName.Autoconsent - private val FEATURE_NAME_VALUE = FEATURE_NAME.value - private const val EMPTY_JSON_STRING = "{}" - } -} diff --git a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/AutoconsentFeatureTogglesPluginTest.kt b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/AutoconsentFeatureTogglesPluginTest.kt deleted file mode 100644 index d92c5a1e93e1..000000000000 --- a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/AutoconsentFeatureTogglesPluginTest.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (c) 2022 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.autoconsent.impl - -import com.duckduckgo.appbuildconfig.api.AppBuildConfig -import com.duckduckgo.autoconsent.api.AutoconsentFeatureName.Autoconsent -import com.duckduckgo.autoconsent.store.AutoconsentFeatureToggleRepository -import kotlinx.coroutines.test.runTest -import org.junit.Assert.* -import org.junit.Before -import org.junit.Test -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever - -class AutoconsentFeatureTogglesPluginTest { - - private val autoconsentFeatureToggleRepository: AutoconsentFeatureToggleRepository = mock() - private lateinit var plugin: AutoconsentFeatureTogglesPlugin - private val mockAppBuildConfig: AppBuildConfig = mock() - - @Before - fun setup() { - plugin = AutoconsentFeatureTogglesPlugin(autoconsentFeatureToggleRepository, mockAppBuildConfig) - } - - @Test - fun whenIsEnabledCalledOnAutoconsentFeatureNameThenReturnRepositoryValue() { - whenever(autoconsentFeatureToggleRepository.get(Autoconsent, false)).thenReturn(true) - assertEquals(true, plugin.isEnabled(Autoconsent.value, false)) - - whenever(autoconsentFeatureToggleRepository.get(Autoconsent, false)).thenReturn(false) - assertEquals(false, plugin.isEnabled(Autoconsent.value, false)) - } - - @Test - fun whenIsEnabledCalledOnOtherFeatureNameThenReturnRepositoryNull() { - assertNull(plugin.isEnabled(TestFeatureName().value, false)) - } - - @Test - fun whenIsEnabledAndFeatureIsAutoconsentThenReturnTrueWhenEnabled() = runTest { - givenAutoconsentFeatureIsEnabled() - - val isEnabled = plugin.isEnabled(Autoconsent.value, true) - - assertTrue(isEnabled!!) - } - - @Test - fun whenIsEnabledAndFeatureIsAutoconsentThenReturnFalseWhenDisabled() = runTest { - givenAutoconsentFeatureIsDisabled() - - val isEnabled = plugin.isEnabled(Autoconsent.value, true) - - assertFalse(isEnabled!!) - } - - @Test - fun whenIsEnabledAndFeatureIsAutoconsentThenReturnDefaultValueIfFeatureDoesNotExist() = runTest { - givenAutoconsentFeatureReturnsDefaultValue(true) - assertTrue(plugin.isEnabled(Autoconsent.value, true)!!) - - givenAutoconsentFeatureReturnsDefaultValue(false) - assertFalse(plugin.isEnabled(Autoconsent.value, false)!!) - } - - @Test - fun whenIsEnabledAndFeatureIsAutoconsentAndAppVersionEqualToMinSupportedVersionThenReturnTrueWhenEnabled() = runTest { - givenAutoconsentFeatureIsEnabled() - givenAppVersionIsEqualToMinSupportedVersion() - - val isEnabled = plugin.isEnabled(Autoconsent.value, true) - - assertTrue(isEnabled!!) - } - - @Test - fun whenIsEnabledAndFeatureIsAutoconsentAndAppVersionIsGreaterThanMinSupportedVersionThenReturnTrueWhenEnabled() = runTest { - givenAutoconsentFeatureIsEnabled() - givenAppVersionIsGreaterThanMinSupportedVersion() - - val isEnabled = plugin.isEnabled(Autoconsent.value, true) - - assertTrue(isEnabled!!) - } - - @Test - fun whenIsEnabledAndFeatureAutoconsentAndAppVersionIsSmallerThanMinSupportedVersionThenReturnFalseWhenEnabled() = runTest { - givenAutoconsentFeatureIsEnabled() - givenAppVersionIsSmallerThanMinSupportedVersion() - - val isEnabled = plugin.isEnabled(Autoconsent.value, true) - - assertFalse(isEnabled!!) - } - - private fun givenAutoconsentFeatureIsEnabled() { - whenever(autoconsentFeatureToggleRepository.get(Autoconsent, true)).thenReturn(true) - } - - private fun givenAutoconsentFeatureIsDisabled() { - whenever(autoconsentFeatureToggleRepository.get(Autoconsent, true)).thenReturn(false) - } - - private fun givenAutoconsentFeatureReturnsDefaultValue(defaultValue: Boolean) { - whenever(autoconsentFeatureToggleRepository.get(Autoconsent, defaultValue)).thenReturn(defaultValue) - } - - private fun givenAppVersionIsEqualToMinSupportedVersion() { - whenever(autoconsentFeatureToggleRepository.getMinSupportedVersion(Autoconsent)).thenReturn(1234) - whenever(mockAppBuildConfig.versionCode).thenReturn(1234) - } - - private fun givenAppVersionIsGreaterThanMinSupportedVersion() { - whenever(autoconsentFeatureToggleRepository.getMinSupportedVersion(Autoconsent)).thenReturn(1234) - whenever(mockAppBuildConfig.versionCode).thenReturn(5678) - } - - private fun givenAppVersionIsSmallerThanMinSupportedVersion() { - whenever(autoconsentFeatureToggleRepository.getMinSupportedVersion(Autoconsent)).thenReturn(1234) - whenever(mockAppBuildConfig.versionCode).thenReturn(123) - } -} - -class TestFeatureName(val value: String = "test") diff --git a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/RealAutoconsentTest.kt b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/RealAutoconsentTest.kt index 63b8aa490279..07b3cf4f1009 100644 --- a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/RealAutoconsentTest.kt +++ b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/RealAutoconsentTest.kt @@ -19,10 +19,11 @@ package com.duckduckgo.autoconsent.impl import android.webkit.WebView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.duckduckgo.autoconsent.api.AutoconsentFeatureName -import com.duckduckgo.autoconsent.store.AutoconsentRepository +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentExceptionsRepository +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeature import com.duckduckgo.feature.toggles.api.FeatureExceptions.FeatureException -import com.duckduckgo.feature.toggles.api.FeatureToggle +import com.duckduckgo.feature.toggles.api.Toggle +import java.util.concurrent.CopyOnWriteArrayList import org.junit.Assert.* import org.junit.Before import org.junit.Test @@ -38,21 +39,24 @@ class RealAutoconsentTest { private val settingsRepository = FakeSettingsRepository() private val unprotected = FakeUnprotected(listOf("unprotected.com")) private val userAllowlist = FakeUserAllowlist(listOf("userallowed.com")) - private val mockAutoconsentRepository: AutoconsentRepository = mock() - private val mockFeatureToggle: FeatureToggle = mock() + private val mockAutoconsentExceptionsRepository: AutoconsentExceptionsRepository = mock() + private val mockAutoconsentFeature: AutoconsentFeature = mock() + private val mockToggle: Toggle = mock() private val webView: WebView = WebView(InstrumentationRegistry.getInstrumentation().targetContext) lateinit var autoconsent: RealAutoconsent @Before fun setup() { - whenever(mockFeatureToggle.isFeatureEnabled(AutoconsentFeatureName.Autoconsent.value)).thenReturn(true) - whenever(mockAutoconsentRepository.exceptions).thenReturn(listOf(FeatureException("exception.com", "reason"))) + whenever(mockAutoconsentFeature.self()).thenReturn(mockToggle) + whenever(mockToggle.isEnabled()).thenReturn(true) + whenever(mockAutoconsentExceptionsRepository.exceptions) + .thenReturn(CopyOnWriteArrayList().apply { add(FeatureException("exception.com", "reason")) }) autoconsent = RealAutoconsent( pluginPoint, settingsRepository, - mockAutoconsentRepository, - mockFeatureToggle, + mockAutoconsentExceptionsRepository, + mockAutoconsentFeature, userAllowlist, unprotected, ) @@ -180,7 +184,7 @@ class RealAutoconsentTest { @Test fun whenInjectAutoconsentIfFeatureIsDisabledThenDoNothing() { givenSettingsRepositoryAllowsInjection() - whenever(mockFeatureToggle.isFeatureEnabled(AutoconsentFeatureName.Autoconsent.value)).thenReturn(false) + whenever(mockToggle.isEnabled()).thenReturn(false) autoconsent.injectAutoconsent(webView, URL) diff --git a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/handlers/InitMessageHandlerPluginTest.kt b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/handlers/InitMessageHandlerPluginTest.kt index b9f100246a05..e307bfbeb99f 100644 --- a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/handlers/InitMessageHandlerPluginTest.kt +++ b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/handlers/InitMessageHandlerPluginTest.kt @@ -23,11 +23,11 @@ import com.duckduckgo.autoconsent.api.AutoconsentCallback import com.duckduckgo.autoconsent.impl.FakeSettingsRepository import com.duckduckgo.autoconsent.impl.adapters.JSONObjectAdapter import com.duckduckgo.autoconsent.impl.handlers.InitMessageHandlerPlugin.InitResp -import com.duckduckgo.autoconsent.store.AutoconsentRepository -import com.duckduckgo.autoconsent.store.DisabledCmpsEntity +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeatureSettingsRepository import com.duckduckgo.common.test.CoroutineTestRule import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import java.util.concurrent.CopyOnWriteArrayList import kotlinx.coroutines.test.TestScope import org.junit.Assert.* import org.junit.Rule @@ -43,7 +43,7 @@ class InitMessageHandlerPluginTest { @get:Rule var coroutineRule = CoroutineTestRule() private val mockCallback: AutoconsentCallback = mock() - private val repository: AutoconsentRepository = mock() + private val repository: AutoconsentFeatureSettingsRepository = mock() private val webView: WebView = WebView(InstrumentationRegistry.getInstrumentation().targetContext) private val settingsRepository = FakeSettingsRepository() @@ -105,7 +105,7 @@ class InitMessageHandlerPluginTest { @Test fun whenProcessMessageForFirstTimeThenDoNotCallEvaluate() { - whenever(repository.disabledCmps).thenReturn(listOf(DisabledCmpsEntity("MyCmp"))) + whenever(repository.disabledCMPs).thenReturn(CopyOnWriteArrayList().apply { add("MyCmp") }) settingsRepository.userSetting = false settingsRepository.firstPopupHandled = false @@ -120,6 +120,7 @@ class InitMessageHandlerPluginTest { fun whenProcessMessageResponseSentIsCorrect() { settingsRepository.userSetting = true settingsRepository.firstPopupHandled = true + whenever(repository.disabledCMPs).thenReturn(CopyOnWriteArrayList()) initHandlerPlugin.process(initHandlerPlugin.supportedTypes.first(), message(), webView, mockCallback) diff --git a/autoconsent/autoconsent-store/src/test/java/com/duckduckgo/autoconsent/store/RealAutoconsentRepositoryTest.kt b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentExceptionsRepositoryTest.kt similarity index 60% rename from autoconsent/autoconsent-store/src/test/java/com/duckduckgo/autoconsent/store/RealAutoconsentRepositoryTest.kt rename to autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentExceptionsRepositoryTest.kt index 51e52ac5044f..75bdd896425c 100644 --- a/autoconsent/autoconsent-store/src/test/java/com/duckduckgo/autoconsent/store/RealAutoconsentRepositoryTest.kt +++ b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentExceptionsRepositoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,12 @@ * limitations under the License. */ -package com.duckduckgo.autoconsent.store +package com.duckduckgo.autoconsent.impl.remoteconfig +import com.duckduckgo.autoconsent.store.AutoconsentDao +import com.duckduckgo.autoconsent.store.AutoconsentDatabase +import com.duckduckgo.autoconsent.store.AutoconsentExceptionEntity +import com.duckduckgo.autoconsent.store.toFeatureException import com.duckduckgo.common.test.CoroutineTestRule import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -29,7 +33,7 @@ import org.mockito.kotlin.reset import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -class RealAutoconsentRepositoryTest { +class RealAutoconsentExceptionsRepositoryTest { @get:Rule var coroutineRule = CoroutineTestRule() @@ -37,56 +41,49 @@ class RealAutoconsentRepositoryTest { private val mockDatabase: AutoconsentDatabase = mock() private val mockDao: AutoconsentDao = mock() - lateinit var repository: AutoconsentRepository + lateinit var repository: AutoconsentExceptionsRepository @Before fun before() { whenever(mockDatabase.autoconsentDao()).thenReturn(mockDao) - - repository = RealAutoconsentRepository(mockDatabase, TestScope(), coroutineRule.testDispatcherProvider) } @Test fun whenRepositoryIsCreatedThenExceptionsLoadedIntoMemory() { - givenDaoContainsExceptionsAndDisabledCmps() + givenDaoContainsExceptions() - repository = RealAutoconsentRepository(mockDatabase, TestScope(), coroutineRule.testDispatcherProvider) + repository = RealAutoconsentExceptionsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase) assertEquals(exception.toFeatureException(), repository.exceptions.first()) - assertEquals(disabledCmp, repository.disabledCmps.first()) } @Test fun whenUpdateAllThenUpdateAllCalled() = runTest { - repository = RealAutoconsentRepository(mockDatabase, TestScope(), coroutineRule.testDispatcherProvider) + repository = RealAutoconsentExceptionsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase) - repository.updateAll(listOf(), listOf()) + repository.insertAllExceptions(listOf()) - verify(mockDao).updateAll(anyList(), anyList()) + verify(mockDao).updateAllExceptions(anyList()) } @Test fun whenUpdateAllThenPreviousExceptionsAreCleared() = runTest { - givenDaoContainsExceptionsAndDisabledCmps() - repository = RealAutoconsentRepository(mockDatabase, TestScope(), coroutineRule.testDispatcherProvider) + givenDaoContainsExceptions() + repository = RealAutoconsentExceptionsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase) assertEquals(1, repository.exceptions.size) - assertEquals(1, repository.disabledCmps.size) reset(mockDao) - repository.updateAll(listOf(), listOf()) + repository.insertAllExceptions(listOf()) assertEquals(0, repository.exceptions.size) - assertEquals(0, repository.disabledCmps.size) } - private fun givenDaoContainsExceptionsAndDisabledCmps() { + private fun givenDaoContainsExceptions() { whenever(mockDao.getExceptions()).thenReturn(listOf(exception)) - whenever(mockDao.getDisabledCmps()).thenReturn(listOf(disabledCmp)) } companion object { val exception = AutoconsentExceptionEntity("example.com", "reason") - val disabledCmp = DisabledCmpsEntity("disabledcmp") } } diff --git a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentFeatureSettingsRepositoryTest.kt b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentFeatureSettingsRepositoryTest.kt new file mode 100644 index 000000000000..3b1d1ba59023 --- /dev/null +++ b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentFeatureSettingsRepositoryTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 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.autoconsent.impl.remoteconfig + +import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeatureModels.AutoconsentSettings +import com.duckduckgo.autoconsent.store.AutoconsentDao +import com.duckduckgo.autoconsent.store.AutoconsentDatabase +import com.duckduckgo.autoconsent.store.DisabledCmpsEntity +import com.duckduckgo.common.test.CoroutineTestRule +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.anyList +import org.mockito.kotlin.mock +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class RealAutoconsentFeatureSettingsRepositoryTest { + + @get:Rule + var coroutineRule = CoroutineTestRule() + + private val mockDatabase: AutoconsentDatabase = mock() + private val mockDao: AutoconsentDao = mock() + + lateinit var repository: AutoconsentFeatureSettingsRepository + + @Before + fun before() { + whenever(mockDatabase.autoconsentDao()).thenReturn(mockDao) + } + + @Test + fun whenRepositoryIsCreatedThenExceptionsLoadedIntoMemory() { + givenDaoContainsDisabledCmps() + + repository = RealAutoconsentFeatureSettingsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase) + + assertEquals(disabledCmpName, repository.disabledCMPs.first()) + } + + @Test + fun whenUpdateAllThenUpdateAllCalled() = runTest { + repository = RealAutoconsentFeatureSettingsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase) + + repository.updateAllSettings(AutoconsentSettings(listOf())) + + verify(mockDao).updateAllDisabledCMPs(anyList()) + } + + @Test + fun whenUpdateAllThenPreviousExceptionsAreCleared() = runTest { + givenDaoContainsDisabledCmps() + repository = RealAutoconsentFeatureSettingsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase) + + assertEquals(1, repository.disabledCMPs.size) + + reset(mockDao) + + repository.updateAllSettings(AutoconsentSettings(listOf())) + + assertEquals(0, repository.disabledCMPs.size) + } + + private fun givenDaoContainsDisabledCmps() { + whenever(mockDao.getDisabledCmps()).thenReturn(listOf(disabledCmp)) + } + + companion object { + const val disabledCmpName = "disabledcmp" + val disabledCmp = DisabledCmpsEntity(disabledCmpName) + } +} diff --git a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModelTest.kt b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModelTest.kt index 2680955421ff..d3b1ef80866b 100644 --- a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModelTest.kt +++ b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/ui/AutoconsentSettingsViewModelTest.kt @@ -92,6 +92,10 @@ class AutoconsentSettingsViewModelTest { override fun isSettingEnabled(): Boolean = test + override fun isAutoconsentEnabled(): Boolean { + return isSettingEnabled() + } + override fun setAutoconsentOptOut(webView: WebView) { // NO OP } diff --git a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentDao.kt b/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentDao.kt index 06306e09b9be..9ca9222086da 100644 --- a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentDao.kt +++ b/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentDao.kt @@ -39,6 +39,18 @@ abstract class AutoconsentDao { insertExceptions(exceptions) } + @Transaction + open fun updateAllDisabledCMPs(disabledCMPs: List) { + deleteDisabledCmps() + insertDisabledCmps(disabledCMPs) + } + + @Transaction + open fun updateAllExceptions(exceptions: List) { + deleteExceptions() + insertExceptions(exceptions) + } + @Query("select * from autoconsent_exceptions") abstract fun getExceptions(): List diff --git a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentFeatureToggleRepository.kt b/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentFeatureToggleRepository.kt deleted file mode 100644 index 7cde84b06908..000000000000 --- a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentFeatureToggleRepository.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2022 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.autoconsent.store - -import android.content.Context - -interface AutoconsentFeatureToggleRepository : AutoconsentFeatureToggleStore { - companion object { - fun create( - context: Context, - ): AutoconsentFeatureToggleRepository { - val store = RealAutoconsentFeatureToggleStore(context) - return RealAutoconsentFeatureToggleRepository(store) - } - } -} - -internal class RealAutoconsentFeatureToggleRepository constructor( - private val autoconsentFeatureToggleStore: AutoconsentFeatureToggleStore, -) : AutoconsentFeatureToggleRepository, AutoconsentFeatureToggleStore by autoconsentFeatureToggleStore diff --git a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentFeatureToggleStore.kt b/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentFeatureToggleStore.kt deleted file mode 100644 index c0de967f8aee..000000000000 --- a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentFeatureToggleStore.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2022 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.autoconsent.store - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import com.duckduckgo.autoconsent.api.AutoconsentFeatureName - -interface AutoconsentFeatureToggleStore { - fun deleteAll() - - fun get( - featureName: AutoconsentFeatureName, - defaultValue: Boolean, - ): Boolean - - fun getMinSupportedVersion(featureName: AutoconsentFeatureName): Int - - fun insert(toggle: AutoconsentFeatureToggles) -} - -internal class RealAutoconsentFeatureToggleStore(private val context: Context) : AutoconsentFeatureToggleStore { - private val preferences: SharedPreferences by lazy { context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) } - - override fun deleteAll() { - preferences.edit().clear().apply() - } - - override fun get(featureName: AutoconsentFeatureName, defaultValue: Boolean): Boolean { - return preferences.getBoolean(featureName.value, defaultValue) - } - - override fun getMinSupportedVersion(featureName: AutoconsentFeatureName): Int { - return preferences.getInt("${featureName.value}$MIN_SUPPORTED_VERSION", 0) - } - - override fun insert(toggle: AutoconsentFeatureToggles) { - preferences.edit { - putBoolean(toggle.featureName.value, toggle.enabled) - toggle.minSupportedVersion?.let { - putInt("${toggle.featureName.value}$MIN_SUPPORTED_VERSION", it) - } - } - } - - companion object { - const val FILENAME = "com.duckduckgo.autoconsent.store.toggles" - const val MIN_SUPPORTED_VERSION = "MinSupportedVersion" - } -} - -data class AutoconsentFeatureToggles( - val featureName: AutoconsentFeatureName, - val enabled: Boolean, - val minSupportedVersion: Int?, -) diff --git a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentSettingsDataStore.kt b/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentSettingsDataStore.kt index 0ff452639b7a..081e5b35e35b 100644 --- a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentSettingsDataStore.kt +++ b/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentSettingsDataStore.kt @@ -25,16 +25,27 @@ interface AutoconsentSettingsDataStore { var firstPopupHandled: Boolean } -class RealAutoconsentSettingsDataStore constructor(private val context: Context) : +class RealAutoconsentSettingsDataStore constructor( + private val context: Context, + onByDefault: Boolean, +) : AutoconsentSettingsDataStore { private val preferences: SharedPreferences by lazy { context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) } + private var internalUserSettings: Boolean + + init { + internalUserSettings = preferences.getBoolean(AUTOCONSENT_USER_SETTING, onByDefault) + } override var userSetting: Boolean - get() = preferences.getBoolean(AUTOCONSENT_USER_SETTING, false) + get() = internalUserSettings + set(value) { preferences.edit(commit = true) { putBoolean(AUTOCONSENT_USER_SETTING, value) + }.also { + internalUserSettings = value } } diff --git a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentSettingsRepository.kt b/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentSettingsRepository.kt index 28374d333cd7..39a3870230ff 100644 --- a/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentSettingsRepository.kt +++ b/autoconsent/autoconsent-store/src/main/java/com/duckduckgo/autoconsent/store/AutoconsentSettingsRepository.kt @@ -22,8 +22,9 @@ interface AutoconsentSettingsRepository : AutoconsentSettingsDataStore { companion object { fun create( context: Context, + onByDefault: Boolean, ): AutoconsentSettingsRepository { - val store = RealAutoconsentSettingsDataStore(context) + val store = RealAutoconsentSettingsDataStore(context, onByDefault) return RealAutoconsentSettingsRepository(store) } } From 570d9435c18df1c7f5621e25580cbd6f48470cf6 Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Thu, 15 Feb 2024 10:47:28 +0000 Subject: [PATCH 06/19] Increase minSdk to 26 (Android 8 / Oreo) (#4177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/488551667048375/1206486003155266/f ### Description Increases the `minSdk` to API 26 (aka, Android 8.0 / Oreo). Other than the `minSdk` change, there should be no actual behaviour changes. This also includes the following changes: - removes dead code that can no longer run due to OS-version guards - removes redundant OS-version guards - removes any now-redundant usages of `@Suppress("NewApi")` that I spotted ### Steps to test this PR - [ ] Ensure CI passes - [ ] Smoke test app, as much as you can 🙏 --- .../vpn/apps/ApplicationInfoExtensions.kt | 21 +-- .../ui/ManageRecentAppsProtectionActivity.kt | 2 +- .../vpn/service/DeviceShieldTileService.kt | 2 - .../android/vpn/service/DnsChangeCallback.kt | 5 +- .../vpn/service/TrackerBlockingVpnService.kt | 3 - .../DeviceShieldAlertNotificationBuilder.kt | 25 +-- .../VpnEnabledNotificationBuilder.kt | 30 ++-- .../VpnReminderNotificationBuilder.kt | 21 +-- .../ui/onboarding/VpnOnboardingActivity.kt | 2 +- .../DeviceShieldTrackerActivity.kt | 2 +- .../flipper/PreferencesFlipperPlugin.kt | 7 +- .../app/browser/BrowserWebViewClientTest.kt | 155 ++++++------------ .../app/browser/DuckDuckGoWebViewTest.kt | 14 +- .../httpauth/WebViewHttpAuthStoreTest.kt | 39 +---- .../espresso/DaxDialogsJourneyTest.kt | 2 +- .../app/browser/BrowserTabFragment.kt | 10 +- .../app/browser/BrowserWebViewClient.kt | 18 +- .../app/browser/SpecialUrlDetector.kt | 29 ++-- .../duckduckgo/app/browser/WebDataManager.kt | 3 +- .../app/browser/applinks/AppLinksHandler.kt | 8 +- .../defaultbrowsing/DefaultBrowserDetector.kt | 3 +- .../DefaultBrowserSystemSettings.kt | 3 - .../app/browser/di/BrowserModule.kt | 4 +- .../browser/httpauth/WebViewHttpAuthStore.kt | 15 +- .../httpauth/db/WebViewStoreExtensions.kt | 30 ---- .../com/duckduckgo/app/di/WidgetModule.kt | 3 +- .../app/global/image/GlobalGlideModule.kt | 34 ---- .../app/global/shortcut/AppShortcutCreator.kt | 16 +- .../app/global/view/ActivityExtension.kt | 3 - .../app/icon/api/AppIconModifier.kt | 6 +- .../app/notification/NotificationRegistrar.kt | 9 - .../onboarding/ui/page/DefaultBrowserPage.kt | 14 +- .../app/permissions/PermissionsActivity.kt | 27 +-- .../app/settings/SettingsActivity.kt | 8 +- .../app/widget/ui/WidgetCapabilities.kt | 6 +- .../app/browser/SpecialUrlDetectorImplTest.kt | 91 +++++----- .../applinks/DuckDuckGoAppLinksHandlerTest.kt | 11 +- .../impl/encoding/UrlUnicodeNormalizer.kt | 59 +------ .../securestorage/DerivedKeySecretFactory.kt | 8 - .../SecureStorageKeyGenerator.kt | 32 +--- .../securestorage/di/SecureStorageModule.kt | 5 - .../encryption/RandomBytesGenerator.kt | 14 +- .../SystemAutofillServiceSuppressor.kt | 12 +- .../management/AutofillClipboardInteractor.kt | 5 +- .../AutofillManagementCredentialsMode.kt | 6 +- .../LegacyUrlUnicodeNormalizerTest.kt | 45 ----- .../UrlUnicodeNormalizerDelegatorTest.kt | 71 -------- ...est.kt => UrlUnicodeNormalizerImplTest.kt} | 4 +- .../RealSecureStorageKeyGeneratorTest.kt | 16 +- build.gradle | 4 +- .../common/ui/notifyme/NotifyMeView.kt | 10 -- .../common/ui/notifyme/NotifyMeViewModel.kt | 8 +- .../ui/notifyme/NotifyMeViewModelTest.kt | 29 ---- .../utils/extensions/ActivityExtensions.kt | 9 +- .../utils/extensions/StringExtensions.kt | 7 +- .../NetworkProtectionManagementActivity.kt | 2 +- .../NetPDisabledNotificationBuilder.kt | 21 +-- .../impl/settings/NetPVpnSettingsActivity.kt | 2 +- .../feature/NetPInternalSettingsActivity.kt | 13 +- .../NetpAccessRevokedNotificationBuilder.kt | 21 +-- .../impl/ui/SubscriptionsWebViewClient.kt | 3 - 61 files changed, 227 insertions(+), 860 deletions(-) rename app/src/{test => androidTest}/java/com/duckduckgo/app/browser/DuckDuckGoWebViewTest.kt (70%) delete mode 100644 app/src/main/java/com/duckduckgo/app/browser/httpauth/db/WebViewStoreExtensions.kt delete mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/LegacyUrlUnicodeNormalizerTest.kt delete mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizerDelegatorTest.kt rename autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/{ModernUrlUnicodeNormalizerTest.kt => UrlUnicodeNormalizerImplTest.kt} (94%) diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ApplicationInfoExtensions.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ApplicationInfoExtensions.kt index 2f561abfad1e..6abc610f1746 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ApplicationInfoExtensions.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ApplicationInfoExtensions.kt @@ -34,28 +34,11 @@ private fun parseAppCategory(category: Int): AppCategory { } fun ApplicationInfo.parseAppCategory(): AppCategory { - return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - parseAppCategory(category) - } else { - AppCategory.Undefined - } -} - -fun ApplicationInfo.getAppCategoryCompat(): Int { - return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - category - } else { - -1 // inlined value of CATEGORY_UNDEFINED - } + return parseAppCategory(category) } fun ApplicationInfo.isGame(): Boolean { - val category = getAppCategoryCompat() - return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - category == ApplicationInfo.CATEGORY_GAME - } else { - category == 0 // inlined value of CATEGORY_GAME - } + return category == ApplicationInfo.CATEGORY_GAME } fun ApplicationInfo.isSystemApp(): Boolean { diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/ManageRecentAppsProtectionActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/ManageRecentAppsProtectionActivity.kt index 9e042077e467..146f26cdddfc 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/ManageRecentAppsProtectionActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/ManageRecentAppsProtectionActivity.kt @@ -121,7 +121,7 @@ class ManageRecentAppsProtectionActivity : private fun bindViews() { binding.manageRecentAppsSkeleton.startShimmer() binding.alwaysOn.setOnClickListener { - this.launchAlwaysOnSystemSettings(appBuildConfig.sdkInt) + this.launchAlwaysOnSystemSettings() } binding.unrestrictedBatteryUsage.setOnClickListener { this.launchIgnoreBatteryOptimizationSettings() diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/DeviceShieldTileService.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/DeviceShieldTileService.kt index 6d2b165244c9..75bb968257e2 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/DeviceShieldTileService.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/DeviceShieldTileService.kt @@ -25,7 +25,6 @@ import android.os.Bundle import android.service.quicksettings.Tile.STATE_ACTIVE import android.service.quicksettings.Tile.STATE_INACTIVE import android.service.quicksettings.TileService -import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.InjectWith @@ -44,7 +43,6 @@ import kotlinx.coroutines.* import logcat.logcat @Suppress("NoHardcodedCoroutineDispatcher") -@RequiresApi(Build.VERSION_CODES.N) // We don't use the DeviceShieldTileService::class as binding key because TileService (Android) class does not // exist in all APIs, and so using it DeviceShieldTileService::class as key would compile but immediately crash // at startup when Java class loader tries to resolve the TileService::class upon Dagger setup diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/DnsChangeCallback.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/DnsChangeCallback.kt index 927848bd24d8..38d7939cd8de 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/DnsChangeCallback.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/DnsChangeCallback.kt @@ -23,9 +23,7 @@ import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest -import android.os.Build import com.duckduckgo.app.di.AppCoroutineScope -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.mobile.android.vpn.network.util.getActiveNetwork import java.net.InetAddress @@ -37,7 +35,6 @@ import logcat.asLog import logcat.logcat class DnsChangeCallback @Inject constructor( - private val appBuildConfig: AppBuildConfig, private val context: Context, @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, @@ -56,7 +53,7 @@ class DnsChangeCallback @Inject constructor( // we only care about changes in the active network if (activeNetwork != null && activeNetwork != network) return@launch - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.O && !same(lastDns, dns)) { + if (!same(lastDns, dns)) { logcat { """ onLinkPropertiesChanged: DNS changed diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt index f88444c10b56..10e9976f274e 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt @@ -787,9 +787,6 @@ class TrackerBlockingVpnService : VpnService(), CoroutineScope by MainScope(), V for (service in manager.getRunningServices(Int.MAX_VALUE)) { if (TrackerBlockingVpnService::class.java.name == service.service.className) { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) { - return service.started - } return true } } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/DeviceShieldAlertNotificationBuilder.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/DeviceShieldAlertNotificationBuilder.kt index 96c96a0a9326..b7cc197d3fc9 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/DeviceShieldAlertNotificationBuilder.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/DeviceShieldAlertNotificationBuilder.kt @@ -22,12 +22,10 @@ import android.app.NotificationManager.IMPORTANCE_DEFAULT import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Build import android.os.ResultReceiver import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.TaskStackBuilder -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.mobile.android.vpn.R import com.duckduckgo.mobile.android.vpn.ui.notification.DeviceShieldNotificationFactory.DeviceShieldNotification @@ -41,10 +39,8 @@ import dagger.Provides object DeviceShieldAlertNotificationBuilderModule { @Provides - fun providesDeviceShieldAlertNotificationBuilder( - appBuildConfig: AppBuildConfig, - ): DeviceShieldAlertNotificationBuilder { - return AndroidDeviceShieldAlertNotificationBuilder(appBuildConfig) + fun providesDeviceShieldAlertNotificationBuilder(): DeviceShieldAlertNotificationBuilder { + return AndroidDeviceShieldAlertNotificationBuilder() } } @@ -63,19 +59,14 @@ interface DeviceShieldAlertNotificationBuilder { ): Notification } -class AndroidDeviceShieldAlertNotificationBuilder constructor( - private val appBuildConfig: AppBuildConfig, -) : DeviceShieldAlertNotificationBuilder { +class AndroidDeviceShieldAlertNotificationBuilder : DeviceShieldAlertNotificationBuilder { - @Suppress("NewApi") // we use appBuildConfig private fun registerAlertChannel(context: Context) { - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.O) { - val notificationManager = NotificationManagerCompat.from(context) - if (notificationManager.getNotificationChannel(VPN_ALERTS_CHANNEL_ID) == null) { - val channel = NotificationChannel(VPN_ALERTS_CHANNEL_ID, VPN_ALERTS_CHANNEL_NAME, IMPORTANCE_DEFAULT) - channel.description = VPN_ALERTS_CHANNEL_DESCRIPTION - notificationManager.createNotificationChannel(channel) - } + val notificationManager = NotificationManagerCompat.from(context) + if (notificationManager.getNotificationChannel(VPN_ALERTS_CHANNEL_ID) == null) { + val channel = NotificationChannel(VPN_ALERTS_CHANNEL_ID, VPN_ALERTS_CHANNEL_NAME, IMPORTANCE_DEFAULT) + channel.description = VPN_ALERTS_CHANNEL_DESCRIPTION + notificationManager.createNotificationChannel(channel) } } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/VpnEnabledNotificationBuilder.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/VpnEnabledNotificationBuilder.kt index 2afe1564dd48..f4f420482371 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/VpnEnabledNotificationBuilder.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/VpnEnabledNotificationBuilder.kt @@ -41,22 +41,20 @@ class VpnEnabledNotificationBuilder { private const val VPN_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION = "Ongoing state of App Tracking Protection" private fun registerOngoingNotificationChannel(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = - NotificationChannel( - VPN_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_ID, - VPN_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME, - IMPORTANCE_LOW, - ) - channel.setShowBadge(false) - channel.description = VPN_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION - val notificationManager = NotificationManagerCompat.from(context) - notificationManager.createNotificationChannel(channel) - /** - * We needed to create a new channel to fix: https://app.asana.com/0/488551667048375/1206484244032061/f - */ - notificationManager.deleteNotificationChannel("com.duckduckgo.mobile.android.vpn.notification.ongoing") - } + val channel = + NotificationChannel( + VPN_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_ID, + VPN_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_NAME, + IMPORTANCE_LOW, + ) + channel.setShowBadge(false) + channel.description = VPN_FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_DESCRIPTION + val notificationManager = NotificationManagerCompat.from(context) + notificationManager.createNotificationChannel(channel) + /** + * We needed to create a new channel to fix: https://app.asana.com/0/488551667048375/1206484244032061/f + */ + notificationManager.deleteNotificationChannel("com.duckduckgo.mobile.android.vpn.notification.ongoing") } fun buildVpnEnabledNotification( diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/VpnReminderNotificationBuilder.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/VpnReminderNotificationBuilder.kt index 8e2a728877a4..0a150a5f58ff 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/VpnReminderNotificationBuilder.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/notification/VpnReminderNotificationBuilder.kt @@ -20,7 +20,6 @@ import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager.IMPORTANCE_DEFAULT import android.content.Context -import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.duckduckgo.di.scopes.AppScope @@ -41,17 +40,15 @@ class RealVpnReminderNotificationBuilder @Inject constructor( ) : VpnReminderNotificationBuilder { private fun registerAlertChannel(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notificationManager = NotificationManagerCompat.from(context) - if (notificationManager.getNotificationChannel(AndroidDeviceShieldAlertNotificationBuilder.VPN_ALERTS_CHANNEL_ID) == null) { - val channel = NotificationChannel( - VPN_ALERTS_CHANNEL_ID, - VPN_ALERTS_CHANNEL_NAME, - IMPORTANCE_DEFAULT, - ) - channel.description = VPN_ALERTS_CHANNEL_DESCRIPTION - notificationManager.createNotificationChannel(channel) - } + val notificationManager = NotificationManagerCompat.from(context) + if (notificationManager.getNotificationChannel(AndroidDeviceShieldAlertNotificationBuilder.VPN_ALERTS_CHANNEL_ID) == null) { + val channel = NotificationChannel( + VPN_ALERTS_CHANNEL_ID, + VPN_ALERTS_CHANNEL_NAME, + IMPORTANCE_DEFAULT, + ) + channel.description = VPN_ALERTS_CHANNEL_DESCRIPTION + notificationManager.createNotificationChannel(channel) } } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnOnboardingActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnOnboardingActivity.kt index 49a458e6c4b8..f41abbeca5c6 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnOnboardingActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnOnboardingActivity.kt @@ -288,7 +288,7 @@ class VpnOnboardingActivity : DuckDuckGoActivity() { fun onVpnConflictDialogGoToSettings() { deviceShieldPixels.didChooseToOpenSettingsFromVpnConflictDialog() - this.launchAlwaysOnSystemSettings(appBuildConfig.sdkInt) + this.launchAlwaysOnSystemSettings() } fun onVpnConflictDialogContinue() { diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt index 3ce4c65ea66d..4c5c34cdf887 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt @@ -445,7 +445,7 @@ class DeviceShieldTrackerActivity : @SuppressLint("InlinedApi") private fun openVPNSettings() { - this.launchAlwaysOnSystemSettings(appBuildConfig.sdkInt) + this.launchAlwaysOnSystemSettings() } fun onVpnConflictDialogContinue() { diff --git a/app-tracking-protection/vpn-internal/src/main/java/com/duckduckgo/vpn/internal/flipper/PreferencesFlipperPlugin.kt b/app-tracking-protection/vpn-internal/src/main/java/com/duckduckgo/vpn/internal/flipper/PreferencesFlipperPlugin.kt index 0287fdf3e24f..749bbbbc88f0 100644 --- a/app-tracking-protection/vpn-internal/src/main/java/com/duckduckgo/vpn/internal/flipper/PreferencesFlipperPlugin.kt +++ b/app-tracking-protection/vpn-internal/src/main/java/com/duckduckgo/vpn/internal/flipper/PreferencesFlipperPlugin.kt @@ -18,7 +18,6 @@ package com.duckduckgo.vpn.internal.flipper import android.content.Context import android.content.SharedPreferences -import android.os.Build import android.preference.PreferenceManager import androidx.core.content.edit import com.duckduckgo.di.scopes.AppScope @@ -202,11 +201,7 @@ class PreferencesFlipperPlugin @Inject constructor(context: Context) : FlipperPl } private fun getDefaultSharedPreferencesName(context: Context): String { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - PreferenceManager.getDefaultSharedPreferencesName(context) - } else { - context.packageName + "_preferences" - } + return PreferenceManager.getDefaultSharedPreferencesName(context) } } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt index f1144693ad18..7ba63218d6e5 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -340,139 +340,86 @@ class BrowserWebViewClientTest { fun whenShouldOverrideThrowsExceptionThenRecordException() { val exception = RuntimeException() whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenThrow(exception) - testee.shouldOverrideUrlLoading(webView, "") + testee.shouldOverrideUrlLoading(webView, webResourceRequest) verify(crashLogger).logCrash(Crash(shortName = "m_webview_should_override", t = exception)) } @Test fun whenAppLinkDetectedAndIsHandledThenReturnTrue() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val urlType = SpecialUrlDetector.UrlType.AppLink(uriString = EXAMPLE_URL) - whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) - whenever(webResourceRequest.isRedirect).thenReturn(false) - whenever(webResourceRequest.isForMainFrame).thenReturn(true) - whenever(listener.handleAppLink(any(), any())).thenReturn(true) - assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) - verify(listener).handleAppLink(urlType, isForMainFrame = true) - } + val urlType = SpecialUrlDetector.UrlType.AppLink(uriString = EXAMPLE_URL) + whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) + whenever(webResourceRequest.isRedirect).thenReturn(false) + whenever(webResourceRequest.isForMainFrame).thenReturn(true) + whenever(listener.handleAppLink(any(), any())).thenReturn(true) + assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) + verify(listener).handleAppLink(urlType, isForMainFrame = true) } @Test fun whenAppLinkDetectedAndIsNotHandledThenReturnFalse() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val urlType = SpecialUrlDetector.UrlType.AppLink(uriString = EXAMPLE_URL) - whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) - whenever(webResourceRequest.isRedirect).thenReturn(false) - whenever(webResourceRequest.isForMainFrame).thenReturn(true) - whenever(listener.handleAppLink(any(), any())).thenReturn(false) - assertFalse(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) - verify(listener).handleAppLink(urlType, isForMainFrame = true) - } + val urlType = SpecialUrlDetector.UrlType.AppLink(uriString = EXAMPLE_URL) + whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) + whenever(webResourceRequest.isRedirect).thenReturn(false) + whenever(webResourceRequest.isForMainFrame).thenReturn(true) + whenever(listener.handleAppLink(any(), any())).thenReturn(false) + assertFalse(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) + verify(listener).handleAppLink(urlType, isForMainFrame = true) } @Test fun whenAppLinkDetectedAndListenerIsNullThenReturnFalse() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())) - .thenReturn(SpecialUrlDetector.UrlType.AppLink(uriString = EXAMPLE_URL)) - testee.webViewClientListener = null - assertFalse(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) - verify(listener, never()).handleAppLink(any(), any()) - } + whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())) + .thenReturn(SpecialUrlDetector.UrlType.AppLink(uriString = EXAMPLE_URL)) + testee.webViewClientListener = null + assertFalse(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) + verify(listener, never()).handleAppLink(any(), any()) } @Test fun whenNonHttpAppLinkDetectedAndIsHandledThenReturnTrue() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL) - whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) - whenever(webResourceRequest.isRedirect).thenReturn(false) - whenever(listener.handleNonHttpAppLink(any())).thenReturn(true) - whenever(webResourceRequest.isForMainFrame).thenReturn(true) - assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) - verify(listener).handleNonHttpAppLink(urlType) - } + val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL) + whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) + whenever(webResourceRequest.isRedirect).thenReturn(false) + whenever(listener.handleNonHttpAppLink(any())).thenReturn(true) + whenever(webResourceRequest.isForMainFrame).thenReturn(true) + assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) + verify(listener).handleNonHttpAppLink(urlType) } @Test fun whenNonHttpAppLinkDetectedAndIsNotForMainframeThenOnlyReturnTrue() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL) - whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) - whenever(webResourceRequest.isRedirect).thenReturn(false) - whenever(listener.handleNonHttpAppLink(any())).thenReturn(true) - whenever(webResourceRequest.isForMainFrame).thenReturn(false) - assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) - verifyNoInteractions(listener) - } - } - - @Test - fun whenNonHttpAppLinkDetectedAndIsHandledOnApiLessThan24ThenReturnTrue() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL) - whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) - whenever(listener.handleNonHttpAppLink(any())).thenReturn(true) - whenever(webResourceRequest.isForMainFrame).thenReturn(true) - assertTrue(testee.shouldOverrideUrlLoading(webView, EXAMPLE_URL)) - verify(listener).handleNonHttpAppLink(urlType) - } + val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL) + whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) + whenever(webResourceRequest.isRedirect).thenReturn(false) + whenever(listener.handleNonHttpAppLink(any())).thenReturn(true) + whenever(webResourceRequest.isForMainFrame).thenReturn(false) + assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) + verifyNoInteractions(listener) } @Test fun whenNonHttpAppLinkDetectedAndIsNotHandledThenReturnFalse() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL) - whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) - whenever(webResourceRequest.isRedirect).thenReturn(false) - whenever(listener.handleNonHttpAppLink(any())).thenReturn(false) - whenever(webResourceRequest.isForMainFrame).thenReturn(true) - assertFalse(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) - verify(listener).handleNonHttpAppLink(urlType) - } - } - - @Test - fun whenNonHttpAppLinkDetectedAndIsNotHandledOnApiLessThan24ThenReturnFalse() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL) - whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) - whenever(listener.handleNonHttpAppLink(any())).thenReturn(false) - assertFalse(testee.shouldOverrideUrlLoading(webView, EXAMPLE_URL)) - verify(listener).handleNonHttpAppLink(urlType) - } + val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL) + whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn(urlType) + whenever(webResourceRequest.isRedirect).thenReturn(false) + whenever(listener.handleNonHttpAppLink(any())).thenReturn(false) + whenever(webResourceRequest.isForMainFrame).thenReturn(true) + assertFalse(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) + verify(listener).handleNonHttpAppLink(urlType) } @Test fun whenNonHttpAppLinkDetectedAndListenerIsNullThenReturnTrue() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn( - SpecialUrlDetector.UrlType.NonHttpAppLink( - EXAMPLE_URL, - Intent(), - EXAMPLE_URL, - ), - ) - testee.webViewClientListener = null - assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) - verify(listener, never()).handleNonHttpAppLink(any()) - } - } - - @Test - fun whenNonHttpAppLinkDetectedAndListenerIsNullOnApiLessThan24ThenReturnTrue() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn( - SpecialUrlDetector.UrlType.NonHttpAppLink( - EXAMPLE_URL, - Intent(), - EXAMPLE_URL, - ), - ) - testee.webViewClientListener = null - assertTrue(testee.shouldOverrideUrlLoading(webView, EXAMPLE_URL)) - verify(listener, never()).handleNonHttpAppLink(any()) - } + whenever(specialUrlDetector.determineType(initiatingUrl = any(), uri = any())).thenReturn( + SpecialUrlDetector.UrlType.NonHttpAppLink( + EXAMPLE_URL, + Intent(), + EXAMPLE_URL, + ), + ) + testee.webViewClientListener = null + assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest)) + verify(listener, never()).handleNonHttpAppLink(any()) } @Test diff --git a/app/src/test/java/com/duckduckgo/app/browser/DuckDuckGoWebViewTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/DuckDuckGoWebViewTest.kt similarity index 70% rename from app/src/test/java/com/duckduckgo/app/browser/DuckDuckGoWebViewTest.kt rename to app/src/androidTest/java/com/duckduckgo/app/browser/DuckDuckGoWebViewTest.kt index c5ebeca4f827..a3a91eefb3c0 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/DuckDuckGoWebViewTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/DuckDuckGoWebViewTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,18 @@ package com.duckduckgo.app.browser -import android.os.Build +import androidx.test.annotation.UiThreadTest import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertFalse import org.junit.Test class DuckDuckGoWebViewTest { - private lateinit var testee: DuckDuckGoWebView - @Test + @UiThreadTest fun whenWebViewInitialisedThenSafeBrowsingDisabled() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - testee = DuckDuckGoWebView(InstrumentationRegistry.getInstrumentation().targetContext) - assertFalse(testee.settings.safeBrowsingEnabled) - } + val context = InstrumentationRegistry.getInstrumentation().targetContext + val testee = DuckDuckGoWebView(context) + assertFalse(testee.settings.safeBrowsingEnabled) } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/httpauth/WebViewHttpAuthStoreTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/httpauth/WebViewHttpAuthStoreTest.kt index 4970a85b44e5..26bf1ac009b9 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/httpauth/WebViewHttpAuthStoreTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/httpauth/WebViewHttpAuthStoreTest.kt @@ -96,37 +96,6 @@ class WebViewHttpAuthStoreTest { } } - @Test - @Suppress("DEPRECATION") - fun whenSetHttpAuthUsernamePasswordApiBelow26ThenInsertHttpAuthEntity() { - for (i in android.os.Build.VERSION_CODES.M..android.os.Build.VERSION_CODES.N_MR1) { - whenever(appBuildConfig.sdkInt).thenReturn(i) - webViewHttpAuthStore.setHttpAuthUsernamePassword( - webView = webView, - host = "host", - realm = "realm", - username = "name", - password = "pass", - ) - } - val times = (android.os.Build.VERSION_CODES.M..android.os.Build.VERSION_CODES.N_MR1).toList().size - verify(webView, times(times)).setHttpAuthUsernamePassword("host", "realm", "name", "pass") - } - - @Test - @Suppress("DEPRECATION") - @SdkSuppress(maxSdkVersion = android.os.Build.VERSION_CODES.N_MR1) - fun whenGetHttpAuthUsernamePasswordApiBelow26ThenReturnWebViewHttpAuthCredentials() { - for (i in android.os.Build.VERSION_CODES.M..android.os.Build.VERSION_CODES.N_MR1) { - whenever(appBuildConfig.sdkInt).thenReturn(i) - whenever(webView.getHttpAuthUsernamePassword("host", "realm")) - .thenReturn(arrayOf("name", "pass")) - val credentials = webViewHttpAuthStore.getHttpAuthUsernamePassword(webView, "host", "realm") - - assertEquals(WebViewHttpAuthCredentials("name", "pass"), credentials) - } - } - @Test fun whenCleanHttpAuthDatabaseThenCleanDatabaseCalled() = runTest { webViewHttpAuthStore.cleanHttpAuthDatabase() @@ -134,13 +103,13 @@ class WebViewHttpAuthStoreTest { } @Test - @SdkSuppress(minSdkVersion = android.os.Build.VERSION_CODES.LOLLIPOP, maxSdkVersion = android.os.Build.VERSION_CODES.O_MR1) - fun whenAppCreatedAndApiBetween21And27ThenJournalModeChangedToDelete() = runTest { - for (i in android.os.Build.VERSION_CODES.LOLLIPOP..android.os.Build.VERSION_CODES.O_MR1) { + @SdkSuppress(minSdkVersion = android.os.Build.VERSION_CODES.O, maxSdkVersion = android.os.Build.VERSION_CODES.O_MR1) + fun whenAppCreatedAndApiBetween26And27ThenJournalModeChangedToDelete() = runTest { + for (i in android.os.Build.VERSION_CODES.O..android.os.Build.VERSION_CODES.O_MR1) { whenever(appBuildConfig.sdkInt).thenReturn(i) webViewHttpAuthStore.onCreate(mockOwner) } - val times = (android.os.Build.VERSION_CODES.LOLLIPOP..android.os.Build.VERSION_CODES.O_MR1).toList().size + val times = (android.os.Build.VERSION_CODES.O..android.os.Build.VERSION_CODES.O_MR1).toList().size verify(mockDatabaseCleaner, times(times)).changeJournalModeToDelete(databaseLocator.getDatabasePath()) } diff --git a/app/src/androidTest/java/com/duckduckgo/espresso/DaxDialogsJourneyTest.kt b/app/src/androidTest/java/com/duckduckgo/espresso/DaxDialogsJourneyTest.kt index 47aaf0e98d34..69369f81b3f9 100644 --- a/app/src/androidTest/java/com/duckduckgo/espresso/DaxDialogsJourneyTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/espresso/DaxDialogsJourneyTest.kt @@ -45,7 +45,7 @@ class DaxDialogsJourneyTest { @Test @UserJourney @FlakyTest - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N, maxSdkVersion = Build.VERSION_CODES.P) + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O, maxSdkVersion = Build.VERSION_CODES.P) fun daxDialogs_supports_default_browser_journey() { onView(isRoot()).perform(waitForView(withId(R.id.primaryCta))) onView(withId(R.id.primaryCta)).perform(click()) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 2343976c50e7..be224f3cf465 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -58,7 +58,6 @@ import android.widget.Toast import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.annotation.AnyThread -import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -1671,7 +1670,6 @@ class BrowserTabFragment : } } - @Suppress("NewApi") // we use appBuildConfig private fun openAppLink(appLink: SpecialUrlDetector.UrlType.AppLink) { if (appLink.appIntent != null) { appLink.appIntent!!.flags = Intent.FLAG_ACTIVITY_NEW_TASK @@ -1680,7 +1678,7 @@ class BrowserTabFragment : } catch (e: SecurityException) { showToast(R.string.unableToOpenLink) } - } else if (appLink.excludedComponents != null && appBuildConfig.sdkInt >= Build.VERSION_CODES.N) { + } else if (appLink.excludedComponents != null) { val title = getString(R.string.appLinkIntentChooserTitle) val chooserIntent = getChooserIntent(appLink.uriString, title, appLink.excludedComponents!!) startActivityOrQuietlyFail(chooserIntent) @@ -1701,7 +1699,6 @@ class BrowserTabFragment : appLinksSnackBar = null } - @RequiresApi(Build.VERSION_CODES.N) private fun getChooserIntent( url: String?, title: String, @@ -2957,11 +2954,8 @@ class BrowserTabFragment : playStoreUtils.launchPlayStore(appPackage) } - @Suppress("NewApi") // we use appBuildConfig private fun launchDefaultBrowser() { - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.N) { - requireActivity().launchDefaultAppActivity() - } + requireActivity().launchDefaultAppActivity() } private fun launchAppTPOnboardingScreen() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index 1ef6cca9a973..d0f3a704a7a8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -20,7 +20,6 @@ import android.graphics.Bitmap import android.net.Uri import android.net.http.SslError import android.net.http.SslError.SSL_UNTRUSTED -import android.os.Build import android.webkit.HttpAuthHandler import android.webkit.RenderProcessGoneDetail import android.webkit.SslErrorHandler @@ -29,7 +28,6 @@ import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient -import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.annotation.UiThread import androidx.annotation.WorkerThread @@ -103,9 +101,8 @@ class BrowserWebViewClient @Inject constructor( private var start: Long? = null /** - * This is the new method of url overriding available from API 24 onwards + * This is the method of url overriding available from API 24 onwards */ - @RequiresApi(Build.VERSION_CODES.N) override fun shouldOverrideUrlLoading( view: WebView, request: WebResourceRequest, @@ -114,18 +111,6 @@ class BrowserWebViewClient @Inject constructor( return shouldOverride(view, url, request.isForMainFrame) } - /** - * * This is the old, deprecated method of url overriding available until API 23 - */ - @Suppress("OverridingDeprecatedMember") - override fun shouldOverrideUrlLoading( - view: WebView, - urlString: String, - ): Boolean { - val url = Uri.parse(urlString) - return shouldOverride(view, url, isForMainFrame = true) - } - /** * API-agnostic implementation of deciding whether to override url or not */ @@ -370,7 +355,6 @@ class BrowserWebViewClient @Inject constructor( } } - @RequiresApi(Build.VERSION_CODES.O) override fun onRenderProcessGone( view: WebView?, detail: RenderProcessGoneDetail?, diff --git a/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt index 571cff115828..9e5de3f79f17 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt @@ -23,9 +23,7 @@ import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.net.Uri -import android.os.Build import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.privacy.config.api.AmpLinkType import com.duckduckgo.privacy.config.api.AmpLinks import com.duckduckgo.privacy.config.api.TrackingParameters @@ -36,7 +34,6 @@ class SpecialUrlDetectorImpl( private val packageManager: PackageManager, private val ampLinks: AmpLinks, private val trackingParameters: TrackingParameters, - private val appBuildConfig: AppBuildConfig, ) : SpecialUrlDetector { override fun determineType(initiatingUrl: String?, uri: Uri): UrlType { @@ -72,22 +69,20 @@ class SpecialUrlDetectorImpl( return UrlType.TrackingParameterLink(cleanedUrl = cleanedUrl) } - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.N) { - try { - val activities = queryActivities(uriString) - val nonBrowserActivities = keepNonBrowserActivities(activities) - - if (nonBrowserActivities.isNotEmpty()) { - nonBrowserActivities.singleOrNull()?.let { resolveInfo -> - val nonBrowserIntent = buildNonBrowserIntent(resolveInfo, uriString) - return UrlType.AppLink(appIntent = nonBrowserIntent, uriString = uriString) - } - val excludedComponents = getExcludedComponents(activities) - return UrlType.AppLink(excludedComponents = excludedComponents, uriString = uriString) + try { + val activities = queryActivities(uriString) + val nonBrowserActivities = keepNonBrowserActivities(activities) + + if (nonBrowserActivities.isNotEmpty()) { + nonBrowserActivities.singleOrNull()?.let { resolveInfo -> + val nonBrowserIntent = buildNonBrowserIntent(resolveInfo, uriString) + return UrlType.AppLink(appIntent = nonBrowserIntent, uriString = uriString) } - } catch (e: URISyntaxException) { - Timber.w(e, "Failed to parse uri $uriString") + val excludedComponents = getExcludedComponents(activities) + return UrlType.AppLink(excludedComponents = excludedComponents, uriString = uriString) } + } catch (e: URISyntaxException) { + Timber.w(e, "Failed to parse uri $uriString") } ampLinks.extractCanonicalFromAmpLink(uriString)?.let { ampLinkType -> diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebDataManager.kt b/app/src/main/java/com/duckduckgo/app/browser/WebDataManager.kt index 60186c8b15cd..0522ea462901 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebDataManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebDataManager.kt @@ -20,7 +20,6 @@ import android.content.Context import android.webkit.WebStorage import android.webkit.WebView import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore -import com.duckduckgo.app.browser.httpauth.db.clearFormDataCompat import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.global.file.FileDeleter import com.duckduckgo.cookies.api.DuckDuckGoCookieManager @@ -70,7 +69,7 @@ class WebViewDataManager @Inject constructor( } private fun clearFormData(webView: WebView) { - webView.clearFormDataCompat() + webView.clearFormData() } /** diff --git a/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksHandler.kt b/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksHandler.kt index a7785a6daac8..492ebffc02f7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksHandler.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksHandler.kt @@ -16,8 +16,6 @@ package com.duckduckgo.app.browser.applinks -import android.os.Build -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.utils.UriString import com.duckduckgo.common.utils.extractDomain import com.duckduckgo.di.scopes.AppScope @@ -39,9 +37,7 @@ interface AppLinksHandler { } @ContributesBinding(AppScope::class) -class DuckDuckGoAppLinksHandler @Inject constructor( - private val appBuildConfig: AppBuildConfig, -) : AppLinksHandler { +class DuckDuckGoAppLinksHandler @Inject constructor() : AppLinksHandler { var previousUrl: String? = null var isAUserQuery = false @@ -55,7 +51,7 @@ class DuckDuckGoAppLinksHandler @Inject constructor( shouldHaltWebNavigation: Boolean, launchAppLink: () -> Unit, ): Boolean { - if (!appLinksEnabled || appBuildConfig.sdkInt < Build.VERSION_CODES.N || !isForMainFrame) { + if (!appLinksEnabled || !isForMainFrame) { return false } diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserDetector.kt index 448f77cb0f1d..43bff58a0281 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserDetector.kt @@ -46,7 +46,8 @@ class AndroidDefaultBrowserDetector @Inject constructor( ) : DefaultBrowserDetector, BrowserFeatureStateReporterPlugin { override fun deviceSupportsDefaultBrowserConfiguration(): Boolean { - return appBuildConfig.sdkInt >= Build.VERSION_CODES.N + // previously was ensuring that device was >= Build.VERSION_CODES.N. Returning true here to minimize further changes. + return true } override fun isDefaultBrowser(): Boolean { diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserSystemSettings.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserSystemSettings.kt index 3b77c6244ab9..2df219a56aa1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserSystemSettings.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserSystemSettings.kt @@ -17,15 +17,12 @@ package com.duckduckgo.app.browser.defaultbrowsing import android.content.Intent -import android.os.Build import android.provider.Settings -import androidx.annotation.RequiresApi import androidx.core.os.bundleOf class DefaultBrowserSystemSettings { companion object { - @RequiresApi(Build.VERSION_CODES.N) fun intent(): Intent { val intent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) intent.putExtra(SETTINGS_SELECT_OPTION_KEY, DEFAULT_BROWSER_APP_OPTION) diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index 0b98af1f7fa6..77a5a0f2fdc7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -66,7 +66,6 @@ import com.duckduckgo.app.surrogates.ResourceSurrogates import com.duckduckgo.app.tabs.ui.GridViewColumnCalculator import com.duckduckgo.app.trackerdetection.CloakedCnameDetector import com.duckduckgo.app.trackerdetection.TrackerDetector -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.cookies.api.CookieManagerProvider import com.duckduckgo.cookies.api.DuckDuckGoCookieManager @@ -174,8 +173,7 @@ class BrowserModule { packageManager: PackageManager, ampLinks: AmpLinks, trackingParameters: TrackingParameters, - appBuildConfig: AppBuildConfig, - ): SpecialUrlDetector = SpecialUrlDetectorImpl(packageManager, ampLinks, trackingParameters, appBuildConfig) + ): SpecialUrlDetector = SpecialUrlDetectorImpl(packageManager, ampLinks, trackingParameters) @Provides fun webViewRequestInterceptor( diff --git a/app/src/main/java/com/duckduckgo/app/browser/httpauth/WebViewHttpAuthStore.kt b/app/src/main/java/com/duckduckgo/app/browser/httpauth/WebViewHttpAuthStore.kt index b2b872b2453e..c6bd6eab14aa 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/httpauth/WebViewHttpAuthStore.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/httpauth/WebViewHttpAuthStore.kt @@ -95,7 +95,6 @@ class RealWebViewHttpAuthStore @Inject constructor( } } - @Suppress("NewApi") // we use appBuildConfig override fun setHttpAuthUsernamePassword( webView: WebView, host: String, @@ -103,25 +102,15 @@ class RealWebViewHttpAuthStore @Inject constructor( username: String, password: String, ) { - if (appBuildConfig.sdkInt >= android.os.Build.VERSION_CODES.O) { - webViewDatabaseProvider.get().setHttpAuthUsernamePassword(host, realm, username, password) - } else { - webView.setHttpAuthUsernamePassword(host, realm, username, password) - } + webViewDatabaseProvider.get().setHttpAuthUsernamePassword(host, realm, username, password) } - @Suppress("NewApi") // we use appBuildConfig override fun getHttpAuthUsernamePassword( webView: WebView, host: String, realm: String, ): WebViewHttpAuthCredentials? { - val credentials = if (appBuildConfig.sdkInt >= android.os.Build.VERSION_CODES.O) { - webViewDatabaseProvider.get().getHttpAuthUsernamePassword(host, realm) - } else { - @Suppress("DEPRECATION") - webView.getHttpAuthUsernamePassword(host, realm) - } ?: return null + val credentials = webViewDatabaseProvider.get().getHttpAuthUsernamePassword(host, realm) ?: return null return WebViewHttpAuthCredentials(username = credentials[0], password = credentials[1]) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/httpauth/db/WebViewStoreExtensions.kt b/app/src/main/java/com/duckduckgo/app/browser/httpauth/db/WebViewStoreExtensions.kt deleted file mode 100644 index eebd3f582942..000000000000 --- a/app/src/main/java/com/duckduckgo/app/browser/httpauth/db/WebViewStoreExtensions.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2020 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.app.browser.httpauth.db - -import android.os.Build -import android.webkit.WebView -import android.webkit.WebViewDatabase - -@Suppress("DEPRECATION") -fun WebView.clearFormDataCompat() { - clearFormData() - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - val webViewDatabase = WebViewDatabase.getInstance(this.context) - webViewDatabase.clearFormData() - } -} diff --git a/app/src/main/java/com/duckduckgo/app/di/WidgetModule.kt b/app/src/main/java/com/duckduckgo/app/di/WidgetModule.kt index 4555d97b1f35..2003c1dfb412 100644 --- a/app/src/main/java/com/duckduckgo/app/di/WidgetModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/WidgetModule.kt @@ -19,7 +19,6 @@ package com.duckduckgo.app.di import android.content.Context import com.duckduckgo.app.widget.ui.AppWidgetCapabilities import com.duckduckgo.app.widget.ui.WidgetCapabilities -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.widget.SearchAndFavoritesGridCalculator import dagger.Module @@ -31,7 +30,7 @@ object WidgetModule { @Provides @SingleInstanceIn(AppScope::class) - fun widgetCapabilities(context: Context, appBuildConfig: AppBuildConfig): WidgetCapabilities = AppWidgetCapabilities(context, appBuildConfig) + fun widgetCapabilities(context: Context): WidgetCapabilities = AppWidgetCapabilities(context) @Provides fun gridCalculator(): SearchAndFavoritesGridCalculator = SearchAndFavoritesGridCalculator() diff --git a/app/src/main/java/com/duckduckgo/app/global/image/GlobalGlideModule.kt b/app/src/main/java/com/duckduckgo/app/global/image/GlobalGlideModule.kt index 3d6b7e6cdb71..d19c9a1efed0 100644 --- a/app/src/main/java/com/duckduckgo/app/global/image/GlobalGlideModule.kt +++ b/app/src/main/java/com/duckduckgo/app/global/image/GlobalGlideModule.kt @@ -25,14 +25,9 @@ import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.module.AppGlideModule import com.duckduckgo.app.browser.BuildConfig -import com.duckduckgo.app.browser.certificates.rootstore.IsrgRootX1 -import com.duckduckgo.app.browser.certificates.rootstore.IsrgRootX2 import java.io.InputStream -import java.security.cert.X509Certificate import okhttp3.Interceptor import okhttp3.OkHttpClient -import okhttp3.tls.HandshakeCertificates -import timber.log.Timber @GlideModule class GlobalGlideModule : AppGlideModule() { @@ -52,35 +47,6 @@ class GlobalGlideModule : AppGlideModule() { } } - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) { - try { - Timber.d("Registering OkHttp-based ModelLoader for GlideUrl") - - val isrgRootX1 = IsrgRootX1(context) - val isrgRootX2 = IsrgRootX2(context) - - val handshakeCertificates = HandshakeCertificates.Builder() - .addTrustedCertificate(isrgRootX1.certificate() as X509Certificate) - .addTrustedCertificate(isrgRootX2.certificate() as X509Certificate) - .addPlatformTrustedCertificates() - .build() - - okHttpClientBuilder - .sslSocketFactory(handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager) - - // use our custom okHttp instead of default HTTPUrlConnection - registry.replace( - GlideUrl::class.java, - InputStream::class.java, - OkHttpUrlLoader.Factory(okHttpClientBuilder.build()), - ) - } catch (t: Throwable) { - Timber.d("Error registering GlideModule for GlideUrl: $t") - super.registerComponents(context, glide, registry) - return - } - } - // use our custom okHttp instead of default HTTPUrlConnection registry.replace( GlideUrl::class.java, diff --git a/app/src/main/java/com/duckduckgo/app/global/shortcut/AppShortcutCreator.kt b/app/src/main/java/com/duckduckgo/app/global/shortcut/AppShortcutCreator.kt index c2175d039ccd..50b733b34f75 100644 --- a/app/src/main/java/com/duckduckgo/app/global/shortcut/AppShortcutCreator.kt +++ b/app/src/main/java/com/duckduckgo/app/global/shortcut/AppShortcutCreator.kt @@ -21,8 +21,6 @@ import android.content.Context import android.content.Intent import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager -import android.os.Build -import androidx.annotation.RequiresApi import androidx.annotation.UiThread import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.graphics.drawable.IconCompat @@ -32,7 +30,6 @@ import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.browser.R import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesTo @@ -52,23 +49,18 @@ class AppShortcutCreatorModule { @IntoSet fun provideAppShortcutCreatorObserver( appShortcutCreator: AppShortcutCreator, - appBuildConfig: AppBuildConfig, ): MainProcessLifecycleObserver { - return AppShortcutCreatorLifecycleObserver(appShortcutCreator, appBuildConfig) + return AppShortcutCreatorLifecycleObserver(appShortcutCreator) } } class AppShortcutCreatorLifecycleObserver( private val appShortcutCreator: AppShortcutCreator, - private val appBuildConfig: AppBuildConfig, ) : MainProcessLifecycleObserver { @UiThread - @Suppress("NewApi") // we use appBuildConfig override fun onCreate(owner: LifecycleOwner) { Timber.i("Configure app shortcuts") - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.N_MR1) { - appShortcutCreator.configureAppShortcuts() - } + appShortcutCreator.configureAppShortcuts() } } @@ -79,7 +71,6 @@ class AppShortcutCreator @Inject constructor( private val dispatchers: DispatcherProvider, ) { - @RequiresApi(Build.VERSION_CODES.N_MR1) fun configureAppShortcuts() { appCoroutineScope.launch(dispatchers.io()) { val shortcutList = mutableListOf() @@ -93,7 +84,6 @@ class AppShortcutCreator @Inject constructor( } } - @RequiresApi(Build.VERSION_CODES.N_MR1) private fun buildNewTabShortcut(context: Context): ShortcutInfo { return ShortcutInfoCompat.Builder(context, SHORTCUT_ID_NEW_TAB) .setShortLabel(context.getString(R.string.newTabMenuItem)) @@ -107,7 +97,6 @@ class AppShortcutCreator @Inject constructor( .build().toShortcutInfo() } - @RequiresApi(Build.VERSION_CODES.N_MR1) private fun buildClearDataShortcut(context: Context): ShortcutInfo { return ShortcutInfoCompat.Builder(context, SHORTCUT_ID_CLEAR_DATA) .setShortLabel(context.getString(R.string.fireMenu)) @@ -121,7 +110,6 @@ class AppShortcutCreator @Inject constructor( .build().toShortcutInfo() } - @RequiresApi(Build.VERSION_CODES.N_MR1) private fun buildBookmarksShortcut(context: Context): ShortcutInfo { val bookmarksActivity = BookmarksActivity.intent(context).also { it.action = Intent.ACTION_VIEW } diff --git a/app/src/main/java/com/duckduckgo/app/global/view/ActivityExtension.kt b/app/src/main/java/com/duckduckgo/app/global/view/ActivityExtension.kt index 79fe2e1b9162..efbded781bed 100644 --- a/app/src/main/java/com/duckduckgo/app/global/view/ActivityExtension.kt +++ b/app/src/main/java/com/duckduckgo/app/global/view/ActivityExtension.kt @@ -20,11 +20,9 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo -import android.os.Build import android.os.Bundle import android.view.View import android.widget.Toast -import androidx.annotation.RequiresApi import androidx.core.app.ActivityOptionsCompat import androidx.fragment.app.FragmentActivity import com.duckduckgo.app.browser.R @@ -39,7 +37,6 @@ fun FragmentActivity.launchExternalActivity(intent: Intent) { } } -@RequiresApi(Build.VERSION_CODES.N) fun Context.launchDefaultAppActivity() { try { val intent = DefaultBrowserSystemSettings.intent() diff --git a/app/src/main/java/com/duckduckgo/app/icon/api/AppIconModifier.kt b/app/src/main/java/com/duckduckgo/app/icon/api/AppIconModifier.kt index 0360d195ec05..74e2d4f2b814 100644 --- a/app/src/main/java/com/duckduckgo/app/icon/api/AppIconModifier.kt +++ b/app/src/main/java/com/duckduckgo/app/icon/api/AppIconModifier.kt @@ -19,7 +19,6 @@ package com.duckduckgo.app.icon.api import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager -import android.os.Build import androidx.annotation.DrawableRes import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.shortcut.AppShortcutCreator @@ -86,7 +85,6 @@ class AppIconModifier @Inject constructor( private val appBuildConfig: AppBuildConfig, ) : IconModifier { - @Suppress("NewApi") // we use appBuildConfig override fun changeIcon( previousIcon: AppIcon, newIcon: AppIcon, @@ -94,9 +92,7 @@ class AppIconModifier @Inject constructor( disable(context, newIcon) enable(context, newIcon) - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.N_MR1) { - appShortcutCreator.configureAppShortcuts() - } + appShortcutCreator.configureAppShortcuts() } private fun enable( diff --git a/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt b/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt index dd4adb956b50..cdbdbf90ae38 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/NotificationRegistrar.kt @@ -16,17 +16,14 @@ package com.duckduckgo.app.notification -import android.annotation.TargetApi import android.app.NotificationChannel import android.app.NotificationManager.IMPORTANCE_NONE import android.content.Context -import android.os.Build.VERSION_CODES.O import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.LifecycleOwner import com.duckduckgo.app.browser.R import com.duckduckgo.app.di.AppCoroutineScope -import com.duckduckgo.app.global.* import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver import com.duckduckgo.app.notification.model.Channel import com.duckduckgo.app.notification.model.NotificationPlugin @@ -42,7 +39,6 @@ import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber @ContributesMultibinding( scope = AppScope::class, @@ -94,14 +90,9 @@ class NotificationRegistrar @Inject constructor( ) private fun registerApp() { - if (appBuildConfig.sdkInt < O) { - Timber.d("No need to register for notification channels on this SDK version") - return - } configureNotificationChannels() } - @TargetApi(O) private fun configureNotificationChannels() { val notificationChannels = channels.map { NotificationChannel(it.id, context.getString(it.name), it.priority) diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPage.kt index 98b3d963bd5b..df382a154090 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPage.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPage.kt @@ -20,7 +20,6 @@ import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle import android.view.Gravity import android.view.LayoutInflater @@ -192,16 +191,13 @@ class DefaultBrowserPage : OnboardingPageFragment(R.layout.content_onboarding_de startActivityForResult(intent, DEFAULT_BROWSER_REQUEST_CODE_DIALOG) } - @Suppress("NewApi") // we use appBuildConfig private fun onLaunchDefaultBrowserSettingsClicked() { userTriedToSetDDGAsDefault = true - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.N) { - val intent = DefaultBrowserSystemSettings.intent() - try { - startActivityForResult(intent, DEFAULT_BROWSER_REQUEST_CODE_SETTINGS) - } catch (e: ActivityNotFoundException) { - Timber.w(e, getString(R.string.cannotLaunchDefaultAppSettings)) - } + val intent = DefaultBrowserSystemSettings.intent() + try { + startActivityForResult(intent, DEFAULT_BROWSER_REQUEST_CODE_SETTINGS) + } catch (e: ActivityNotFoundException) { + Timber.w(e, getString(R.string.cannotLaunchDefaultAppSettings)) } } diff --git a/app/src/main/java/com/duckduckgo/app/permissions/PermissionsActivity.kt b/app/src/main/java/com/duckduckgo/app/permissions/PermissionsActivity.kt index 9fff4d9b06c4..6949f5852592 100644 --- a/app/src/main/java/com/duckduckgo/app/permissions/PermissionsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/permissions/PermissionsActivity.kt @@ -19,10 +19,8 @@ package com.duckduckgo.app.permissions import android.annotation.SuppressLint import android.app.ActivityOptions import android.content.Intent -import android.os.Build import android.os.Bundle import android.provider.Settings -import android.view.View import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle @@ -65,7 +63,6 @@ class PermissionsActivity : DuckDuckGoActivity() { setupToolbar(binding.includeToolbar.toolbar) configureUiEventHandlers() - configureAppLinksSettingVisibility() observeViewModel() } @@ -82,12 +79,6 @@ class PermissionsActivity : DuckDuckGoActivity() { binding.includePermissions.appLinksSetting.setClickListener { viewModel.userRequestedToChangeAppLinkSetting() } } - private fun configureAppLinksSettingVisibility() { - if (appBuildConfig.sdkInt < Build.VERSION_CODES.N) { - binding.includePermissions.appLinksSetting.visibility = View.GONE - } - } - private fun observeViewModel() { viewModel.viewState() .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) @@ -155,22 +146,10 @@ class PermissionsActivity : DuckDuckGoActivity() { @SuppressLint("InlinedApi") private fun launchNotificationsSettings() { - val settingsIntent = if (appBuildConfig.sdkInt >= Build.VERSION_CODES.O) { - Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(Settings.EXTRA_APP_PACKAGE, packageName) - } else { - Intent(ANDROID_M_APP_NOTIFICATION_SETTINGS) - .putExtra(ANDROID_M_APP_PACKAGE, packageName) - .putExtra(ANDROID_M_APP_UID, applicationInfo.uid) - } + val settingsIntent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(Settings.EXTRA_APP_PACKAGE, packageName) startActivity(settingsIntent, null) } - - companion object { - private const val ANDROID_M_APP_NOTIFICATION_SETTINGS = "android.settings.APP_NOTIFICATION_SETTINGS" - private const val ANDROID_M_APP_PACKAGE = "app_package" - private const val ANDROID_M_APP_UID = "app_uid" - } } diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt index 5a2ae77f9112..27459bd5feda 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt @@ -19,7 +19,6 @@ package com.duckduckgo.app.settings import android.app.ActivityOptions import android.content.Context import android.content.Intent -import android.os.Build import android.os.Bundle import android.view.View import androidx.core.view.isVisible @@ -325,13 +324,8 @@ class SettingsActivity : DuckDuckGoActivity() { } } - @Suppress("NewApi") // we use appBuildConfig private fun launchDefaultAppScreen() { - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.N) { - launchDefaultAppActivity() - } else { - throw IllegalStateException("Unable to launch default app activity on this OS") - } + launchDefaultAppActivity() } private fun launchAutofillSettings() { diff --git a/app/src/main/java/com/duckduckgo/app/widget/ui/WidgetCapabilities.kt b/app/src/main/java/com/duckduckgo/app/widget/ui/WidgetCapabilities.kt index 4104b14fb7e3..9e87414b7aab 100644 --- a/app/src/main/java/com/duckduckgo/app/widget/ui/WidgetCapabilities.kt +++ b/app/src/main/java/com/duckduckgo/app/widget/ui/WidgetCapabilities.kt @@ -19,8 +19,6 @@ package com.duckduckgo.app.widget.ui import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context -import android.os.Build -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.widget.SearchAndFavoritesWidget import com.duckduckgo.widget.SearchWidget import com.duckduckgo.widget.SearchWidgetLight @@ -33,12 +31,10 @@ interface WidgetCapabilities { class AppWidgetCapabilities @Inject constructor( private val context: Context, - private val appBuildConfig: AppBuildConfig, ) : WidgetCapabilities { override val supportsAutomaticWidgetAdd: Boolean - @Suppress("NewApi") // we use appBuildConfig - get() = appBuildConfig.sdkInt >= Build.VERSION_CODES.O && AppWidgetManager.getInstance(context).isRequestPinAppWidgetSupported + get() = AppWidgetManager.getInstance(context).isRequestPinAppWidgetSupported override val hasInstalledWidgets: Boolean get() = context.hasInstalledWidgets diff --git a/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt b/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt index 23e1c2517827..603995de7577 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt @@ -21,7 +21,6 @@ import android.content.IntentFilter import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo -import android.os.Build import androidx.core.net.toUri import androidx.test.ext.junit.runners.AndroidJUnit4 import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.* @@ -69,7 +68,6 @@ class SpecialUrlDetectorImplTest { packageManager = mockPackageManager, ampLinks = mockAmpLinks, trackingParameters = mockTrackingParameters, - appBuildConfig = appBuildConfig, ) whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(emptyList()) } @@ -116,61 +114,48 @@ class SpecialUrlDetectorImplTest { @Test fun whenOneNonBrowserActivityFoundThenReturnAppLinkWithIntent() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn( - listOf( - buildAppResolveInfo(), - buildBrowserResolveInfo(), - ResolveInfo(), - ), - ) - val type = testee.determineType("https://example.com") - verify(mockPackageManager).queryIntentActivities( - argThat { hasCategory(Intent.CATEGORY_BROWSABLE) }, - eq(PackageManager.GET_RESOLVED_FILTER), - ) - assertTrue(type is AppLink) - val appLinkType = type as AppLink - assertEquals("https://example.com", appLinkType.uriString) - assertEquals(EXAMPLE_APP_PACKAGE, appLinkType.appIntent!!.component!!.packageName) - assertEquals(EXAMPLE_APP_ACTIVITY_NAME, appLinkType.appIntent!!.component!!.className) - assertNull(appLinkType.excludedComponents) - } + whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn( + listOf( + buildAppResolveInfo(), + buildBrowserResolveInfo(), + ResolveInfo(), + ), + ) + val type = testee.determineType("https://example.com") + verify(mockPackageManager).queryIntentActivities( + argThat { hasCategory(Intent.CATEGORY_BROWSABLE) }, + eq(PackageManager.GET_RESOLVED_FILTER), + ) + assertTrue(type is AppLink) + val appLinkType = type as AppLink + assertEquals("https://example.com", appLinkType.uriString) + assertEquals(EXAMPLE_APP_PACKAGE, appLinkType.appIntent!!.component!!.packageName) + assertEquals(EXAMPLE_APP_ACTIVITY_NAME, appLinkType.appIntent!!.component!!.className) + assertNull(appLinkType.excludedComponents) } @Test fun whenMultipleNonBrowserActivitiesFoundThenReturnAppLinkWithExcludedComponents() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn( - listOf( - buildAppResolveInfo(), - buildAppResolveInfo(), - buildBrowserResolveInfo(), - ResolveInfo(), - ), - ) - val type = testee.determineType("https://example.com") - verify(mockPackageManager).queryIntentActivities( - argThat { hasCategory(Intent.CATEGORY_BROWSABLE) }, - eq(PackageManager.GET_RESOLVED_FILTER), - ) - assertTrue(type is AppLink) - val appLinkType = type as AppLink - assertEquals("https://example.com", appLinkType.uriString) - assertEquals(1, appLinkType.excludedComponents!!.size) - assertEquals(EXAMPLE_BROWSER_PACKAGE, appLinkType.excludedComponents!![0].packageName) - assertEquals(EXAMPLE_BROWSER_ACTIVITY_NAME, appLinkType.excludedComponents!![0].className) - assertNull(appLinkType.appIntent) - } - } - - @Test - fun whenAppLinkCheckedOnApiLessThan24ThenReturnWebType() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - val type = testee.determineType("https://example.com") - verifyNoInteractions(mockPackageManager) - assertTrue(type is Web) - } + whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn( + listOf( + buildAppResolveInfo(), + buildAppResolveInfo(), + buildBrowserResolveInfo(), + ResolveInfo(), + ), + ) + val type = testee.determineType("https://example.com") + verify(mockPackageManager).queryIntentActivities( + argThat { hasCategory(Intent.CATEGORY_BROWSABLE) }, + eq(PackageManager.GET_RESOLVED_FILTER), + ) + assertTrue(type is AppLink) + val appLinkType = type as AppLink + assertEquals("https://example.com", appLinkType.uriString) + assertEquals(1, appLinkType.excludedComponents!!.size) + assertEquals(EXAMPLE_BROWSER_PACKAGE, appLinkType.excludedComponents!![0].packageName) + assertEquals(EXAMPLE_BROWSER_ACTIVITY_NAME, appLinkType.excludedComponents!![0].className) + assertNull(appLinkType.appIntent) } @Test diff --git a/app/src/test/java/com/duckduckgo/app/browser/applinks/DuckDuckGoAppLinksHandlerTest.kt b/app/src/test/java/com/duckduckgo/app/browser/applinks/DuckDuckGoAppLinksHandlerTest.kt index 78575541515f..18a2470a46d0 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/applinks/DuckDuckGoAppLinksHandlerTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/applinks/DuckDuckGoAppLinksHandlerTest.kt @@ -16,17 +16,16 @@ package com.duckduckgo.app.browser.applinks -import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.duckduckgo.appbuildconfig.api.AppBuildConfig -import junit.framework.TestCase.* +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions -import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class DuckDuckGoAppLinksHandlerTest { @@ -34,12 +33,10 @@ class DuckDuckGoAppLinksHandlerTest { private lateinit var testee: DuckDuckGoAppLinksHandler private var mockCallback: () -> Unit = mock() - private val appBuildConfig: AppBuildConfig = mock() @Before fun setup() { - whenever(appBuildConfig.sdkInt).thenReturn(Build.VERSION_CODES.N) - testee = DuckDuckGoAppLinksHandler(appBuildConfig) + testee = DuckDuckGoAppLinksHandler() testee.previousUrl = "example.com" } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizer.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizer.kt index 767497f7ab48..16de7e1b4d6c 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizer.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizer.kt @@ -17,14 +17,9 @@ package com.duckduckgo.autofill.impl.encoding import android.icu.text.IDNA -import android.os.Build.VERSION_CODES -import androidx.annotation.RequiresApi -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding -import java.net.IDN import javax.inject.Inject -import javax.inject.Named import timber.log.Timber interface UrlUnicodeNormalizer { @@ -33,35 +28,7 @@ interface UrlUnicodeNormalizer { } @ContributesBinding(AppScope::class) -class UrlUnicodeNormalizerDelegator @Inject constructor( - @Named("legacyUnicodeNormalizer") private val legacyNormalizer: UrlUnicodeNormalizer, - @Named("modernUnicodeNormalizer") private val modernNormalizer: UrlUnicodeNormalizer, - private val appBuildConfig: AppBuildConfig, -) : UrlUnicodeNormalizer { - - override fun normalizeAscii(url: String?): String? { - return if (shouldUseModernNormalizer()) { - modernNormalizer.normalizeAscii(url) - } else { - legacyNormalizer.normalizeAscii(url) - } - } - - override fun normalizeUnicode(url: String?): String? { - return if (shouldUseModernNormalizer()) { - modernNormalizer.normalizeUnicode(url) - } else { - legacyNormalizer.normalizeUnicode(url) - } - } - - private fun shouldUseModernNormalizer() = appBuildConfig.sdkInt >= VERSION_CODES.N -} - -@RequiresApi(VERSION_CODES.N) -@ContributesBinding(AppScope::class) -@Named("modernUnicodeNormalizer") -class ModernUrlUnicodeNormalizer @Inject constructor() : UrlUnicodeNormalizer { +class UrlUnicodeNormalizerImpl @Inject constructor() : UrlUnicodeNormalizer { override fun normalizeAscii(url: String?): String? { if (url == null) return null @@ -93,30 +60,6 @@ class ModernUrlUnicodeNormalizer @Inject constructor() : UrlUnicodeNormalizer { } } -@ContributesBinding(AppScope::class) -@Named("legacyUnicodeNormalizer") -class LegacyUrlUnicodeNormalizer @Inject constructor() : UrlUnicodeNormalizer { - - override fun normalizeAscii(url: String?): String? { - if (url == null) return null - - val originalScheme = url.scheme() ?: "" - val noScheme = url.removePrefix(originalScheme) - - return kotlin.runCatching { - IDN.toASCII(noScheme, IDN.ALLOW_UNASSIGNED) - }.getOrNull() ?: url - } - - override fun normalizeUnicode(url: String?): String? { - if (url == null) return null - - return kotlin.runCatching { - IDN.toUnicode(url, IDN.ALLOW_UNASSIGNED) - }.getOrNull() ?: url - } -} - private fun String.scheme(): String? { if (this.startsWith("http://") || this.startsWith("https://")) { return this.substring(0, this.indexOf("://") + 3) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/DerivedKeySecretFactory.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/DerivedKeySecretFactory.kt index 05d8001134f8..becaf0104098 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/DerivedKeySecretFactory.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/DerivedKeySecretFactory.kt @@ -31,11 +31,3 @@ class RealDerivedKeySecretFactory : DerivedKeySecretFactory { override fun getKey(spec: PBEKeySpec): Key = secretKeyFactory.generateSecret(spec) } - -class LegacyDerivedKeySecretFactory : DerivedKeySecretFactory { - private val secretKeyFactory by lazy { - SecretKeyFactory.getInstance("PBKDF2withHmacSHA1") - } - - override fun getKey(spec: PBEKeySpec): Key = secretKeyFactory.generateSecret(spec) -} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorageKeyGenerator.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorageKeyGenerator.kt index a6f5f439dc94..fe6067c000df 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorageKeyGenerator.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorageKeyGenerator.kt @@ -16,9 +16,7 @@ package com.duckduckgo.autofill.impl.securestorage -import android.os.Build import android.security.keystore.KeyProperties -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import java.security.Key @@ -40,9 +38,7 @@ interface SecureStorageKeyGenerator { @ContributesBinding(AppScope::class) class RealSecureStorageKeyGenerator @Inject constructor( - private val appBuildConfig: AppBuildConfig, @Named("DerivedKeySecretFactoryFor26Up") private val derivedKeySecretFactory: Provider, - @Named("DerivedKeySecretFactoryForLegacy") private val legacyDerivedKeySecretFactory: Provider, ) : SecureStorageKeyGenerator { private val keyGenerator: KeyGenerator by lazy { KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES).also { @@ -61,31 +57,19 @@ class RealSecureStorageKeyGenerator @Inject constructor( password: String, salt: ByteArray, ): Key = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.O) { - derivedKeySecretFactory.get().getKey( - PBEKeySpec( - password.toCharArray(), - salt, - ITERATIONS_26_UP, - SIZE, - ), - ) - } else { - legacyDerivedKeySecretFactory.get().getKey( - PBEKeySpec( - password.toCharArray(), - salt, - ITERATIONS_LEGACY, - SIZE, - ), - ) - }.run { + derivedKeySecretFactory.get().getKey( + PBEKeySpec( + password.toCharArray(), + salt, + ITERATIONS_26_UP, + SIZE, + ), + ).run { SecretKeySpec(this.encoded, KeyProperties.KEY_ALGORITHM_AES) } companion object { private const val ITERATIONS_26_UP = 100_000 - private const val ITERATIONS_LEGACY = 50_000 private const val SIZE = 256 } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/di/SecureStorageModule.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/di/SecureStorageModule.kt index d8367b000a92..dc79d3a09d46 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/di/SecureStorageModule.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/di/SecureStorageModule.kt @@ -18,7 +18,6 @@ package com.duckduckgo.autofill.impl.securestorage.di import android.content.Context import com.duckduckgo.autofill.impl.securestorage.DerivedKeySecretFactory -import com.duckduckgo.autofill.impl.securestorage.LegacyDerivedKeySecretFactory import com.duckduckgo.autofill.impl.securestorage.RealDerivedKeySecretFactory import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.securestorage.store.RealSecureStorageKeyRepository @@ -46,8 +45,4 @@ object SecureStorageKeyModule { @Provides @Named("DerivedKeySecretFactoryFor26Up") fun provideDerivedKeySecretFactoryFor26Up(): DerivedKeySecretFactory = RealDerivedKeySecretFactory() - - @Provides - @Named("DerivedKeySecretFactoryForLegacy") - fun provideDerivedKeySecretFactoryForLegacy(): DerivedKeySecretFactory = LegacyDerivedKeySecretFactory() } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/encryption/RandomBytesGenerator.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/encryption/RandomBytesGenerator.kt index 47a659876c53..2b4c2c81fe92 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/encryption/RandomBytesGenerator.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/encryption/RandomBytesGenerator.kt @@ -16,9 +16,6 @@ package com.duckduckgo.securestorage.impl.encryption -import android.annotation.SuppressLint -import android.os.Build -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import java.security.SecureRandom @@ -32,18 +29,11 @@ interface RandomBytesGenerator { } @ContributesBinding(AppScope::class) -class RealRandomBytesGenerator @Inject constructor( - private val appBuildConfig: AppBuildConfig, -) : RandomBytesGenerator { +class RealRandomBytesGenerator @Inject constructor() : RandomBytesGenerator { - @SuppressLint("NewApi") override fun generateBytes(size: Int): ByteArray { return ByteArray(size).apply { - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.O) { - SecureRandom.getInstanceStrong().nextBytes(this) - } else { - SecureRandom().nextBytes(this) - } + SecureRandom.getInstanceStrong().nextBytes(this) } } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/systemautofill/SystemAutofillServiceSuppressor.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/systemautofill/SystemAutofillServiceSuppressor.kt index dc3f183ef974..dc35f0977830 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/systemautofill/SystemAutofillServiceSuppressor.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/systemautofill/SystemAutofillServiceSuppressor.kt @@ -16,11 +16,8 @@ package com.duckduckgo.autofill.impl.systemautofill -import android.annotation.SuppressLint -import android.os.Build import android.view.autofill.AutofillManager import android.webkit.WebView -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject @@ -30,14 +27,9 @@ interface SystemAutofillServiceSuppressor { } @ContributesBinding(AppScope::class) -class RealSystemAutofillServiceSuppressor @Inject constructor( - private val appBuildConfig: AppBuildConfig, -) : SystemAutofillServiceSuppressor { +class RealSystemAutofillServiceSuppressor @Inject constructor() : SystemAutofillServiceSuppressor { - @SuppressLint("NewApi") override fun suppressAutofill(webView: WebView?) { - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.O) { - webView?.context?.getSystemService(AutofillManager::class.java)?.cancel() - } + webView?.context?.getSystemService(AutofillManager::class.java)?.cancel() } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillClipboardInteractor.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillClipboardInteractor.kt index 29f48c42175f..7bac2d67aedc 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillClipboardInteractor.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillClipboardInteractor.kt @@ -20,9 +20,7 @@ import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.os.Build import android.os.PersistableBundle -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.ActivityScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject @@ -34,14 +32,13 @@ interface AutofillClipboardInteractor { @ContributesBinding(ActivityScope::class) class RealAutofillClipboardInteractor @Inject constructor( context: Context, - private val appBuildConfig: AppBuildConfig, ) : AutofillClipboardInteractor { private val clipboardManager by lazy { context.getSystemService(ClipboardManager::class.java) } @SuppressLint("NewApi") override fun copyToClipboard(toCopy: String, isSensitive: Boolean) { val clipData = ClipData.newPlainText("", toCopy) - if (isSensitive && appBuildConfig.sdkInt >= Build.VERSION_CODES.N) { + if (isSensitive) { clipData.description.extras = PersistableBundle().apply { putBoolean("android.content.extra.IS_SENSITIVE", true) } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementCredentialsMode.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementCredentialsMode.kt index bc0def6ee21c..582d9a1eb87d 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementCredentialsMode.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementCredentialsMode.kt @@ -18,8 +18,6 @@ package com.duckduckgo.autofill.impl.ui.credential.management.viewing import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable -import android.os.Build.VERSION -import android.os.Build.VERSION_CODES import android.os.Bundle import android.view.Menu import android.view.MenuInflater @@ -492,9 +490,7 @@ class AutofillManagementCredentialsMode : DuckDuckGoFragment(R.layout.fragment_a } private fun disableSystemAutofillServiceOnPasswordField() { - if (VERSION.SDK_INT >= VERSION_CODES.O) { - binding.passwordEditText.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS - } + binding.passwordEditText.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS } private fun String.convertBlankToNull(): String? = this.ifBlank { null } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/LegacyUrlUnicodeNormalizerTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/LegacyUrlUnicodeNormalizerTest.kt deleted file mode 100644 index 975b8e19a962..000000000000 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/LegacyUrlUnicodeNormalizerTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.autofill.impl.encoding - -import org.junit.Assert.* -import org.junit.Test - -class LegacyUrlUnicodeNormalizerTest { - - private val testee = LegacyUrlUnicodeNormalizer() - - @Test - fun whenNormalizingToAsciiAndContainsNonAsciiThenOutputIdnaEncoded() { - assertEquals("xn--7ca.com", testee.normalizeAscii("ç.com")) - } - - @Test - fun whenNormalizingToAsciiAndOnlyContainsAsciiThenThenInputAndOutputIdentical() { - assertEquals("c.com", testee.normalizeAscii("c.com")) - } - - @Test - fun whenNormalizingToUnicodeAndContainsNonAsciiThenOutputContainsNonAscii() { - assertEquals("ç.com", testee.normalizeUnicode("xn--7ca.com")) - } - - @Test - fun whenNormalizingToUnicodeAndOnlyContainsAsciiThenThenInputAndOutputIdentical() { - assertEquals("c.com", testee.normalizeUnicode("c.com")) - } -} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizerDelegatorTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizerDelegatorTest.kt deleted file mode 100644 index de37a253766d..000000000000 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizerDelegatorTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.autofill.impl.encoding - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.duckduckgo.appbuildconfig.api.AppBuildConfig -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -@RunWith(AndroidJUnit4::class) -class UrlUnicodeNormalizerDelegatorTest { - - private val legacy: UrlUnicodeNormalizer = mock() - private val modern: UrlUnicodeNormalizer = mock() - private val appBuildConfig: AppBuildConfig = mock() - private val testee = UrlUnicodeNormalizerDelegator(legacyNormalizer = legacy, modernNormalizer = modern, appBuildConfig = appBuildConfig) - - @Test - fun whenSdkOlderThan24AndNormalizeToUnicodeCalledThenLegacyNormalizerUsed() { - configureOldAndroidVersion() - testee.normalizeUnicode("") - verify(legacy).normalizeUnicode(any()) - } - - @Test - fun whenSdkOlderThan24AndNormalizeToAsciiCalledThenLegacyNormalizerUsed() { - configureOldAndroidVersion() - testee.normalizeAscii("") - verify(legacy).normalizeAscii(any()) - } - - @Test - fun whenSdkIsModernEnoughThanAndNormalizeToUnicodeCalledThenModernNormalizerUsed() { - configureModernAndroidVersion() - testee.normalizeUnicode("") - verify(modern).normalizeUnicode(any()) - } - - @Test - fun whenSdkIsModernEnoughThanAndNormalizeToAsciiCalledThenModernNormalizerUsed() { - configureModernAndroidVersion() - testee.normalizeAscii("") - verify(modern).normalizeAscii(any()) - } - - private fun configureOldAndroidVersion() { - whenever(appBuildConfig.sdkInt).thenReturn(23) - } - - private fun configureModernAndroidVersion() { - whenever(appBuildConfig.sdkInt).thenReturn(24) - } -} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/ModernUrlUnicodeNormalizerTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizerImplTest.kt similarity index 94% rename from autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/ModernUrlUnicodeNormalizerTest.kt rename to autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizerImplTest.kt index 015380a0124b..b133cc7c16e6 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/ModernUrlUnicodeNormalizerTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/encoding/UrlUnicodeNormalizerImplTest.kt @@ -22,9 +22,9 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class ModernUrlUnicodeNormalizerTest { +class UrlUnicodeNormalizerImplTest { - private val testee = ModernUrlUnicodeNormalizer() + private val testee = UrlUnicodeNormalizerImpl() @Test fun whenNormalizingToAsciiAndContainsNonAsciiThenOutputIdnaEncoded() { diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/securestorage/RealSecureStorageKeyGeneratorTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/securestorage/RealSecureStorageKeyGeneratorTest.kt index 6c7bad8dfce0..47275c9a38f3 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/securestorage/RealSecureStorageKeyGeneratorTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/securestorage/RealSecureStorageKeyGeneratorTest.kt @@ -51,11 +51,7 @@ class RealSecureStorageKeyGeneratorTest { whenever(derivedKeySecretFactory.getKey(any())).thenReturn(key) whenever(legacyDerivedKeySecretFactory.getKey(any())).thenReturn(key) - testee = RealSecureStorageKeyGenerator( - appBuildConfig, - { derivedKeySecretFactory }, - { legacyDerivedKeySecretFactory }, - ) + testee = RealSecureStorageKeyGenerator { derivedKeySecretFactory } } @Test @@ -79,16 +75,6 @@ class RealSecureStorageKeyGeneratorTest { assertEquals("AES", result.algorithm) } - @Test - fun whenKeyIsGeneratedFromPasswordForSDK25MaterialThenUseLegacyDerivedKeySecretFactoryAndAlgorithmShouldBeAES() { - whenever(appBuildConfig.sdkInt).thenReturn(25) - - val result = testee.generateKeyFromPassword("password", randomBytes) - - verify(legacyDerivedKeySecretFactory).getKey(any()) - assertEquals("AES", result.algorithm) - } - companion object { private val randomBytes = "Zm9vYg==".toByteArray() } diff --git a/build.gradle b/build.gradle index 2acf28e1fd16..650b3aa6228f 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { anvil_version = '2.4.9' ksp_version = '1.9.20-1.0.14' gradle_plugin = '8.1.2' - min_sdk = 23 + min_sdk = 26 target_sdk = 34 compile_sdk = 34 fladle_version = '0.17.4' @@ -182,7 +182,7 @@ fladle { ]) devices.set([ ["model": "Pixel3", "version": "30"], - ["model": "Nexus6", "version": "23"] + ["model": "Pixel2.arm", "version": "28"] ]) localResultsDir.set("fladleResults") } diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/notifyme/NotifyMeView.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/notifyme/NotifyMeView.kt index eb13ea5daee1..a92cab4bae1f 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/notifyme/NotifyMeView.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/notifyme/NotifyMeView.kt @@ -43,7 +43,6 @@ import com.duckduckgo.common.ui.notifyme.NotifyMeView.Orientation.Center import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.CheckPermissionRationale import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.DismissComponent -import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.OpenSettings import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.OpenSettingsOnAndroid8Plus import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.ShowPermissionRationale import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.UpdateNotificationsState @@ -204,7 +203,6 @@ class NotifyMeView @JvmOverloads constructor( when (command) { is UpdateNotificationsState -> updateNotificationsState() is UpdateNotificationsStateOnAndroid13Plus -> updateNotificationsPermissionsOnAndroid13Plus() - is OpenSettings -> openSettings() is OpenSettingsOnAndroid8Plus -> openSettingsOnAndroid8Plus() is DismissComponent -> hideMe() is CheckPermissionRationale -> checkPermissionRationale() @@ -224,14 +222,6 @@ class NotifyMeView @JvmOverloads constructor( viewModel.updateNotificationsPermissions(granted) } - private fun openSettings() { - val settingsIntent = Intent(ANDROID_M_APP_NOTIFICATION_SETTINGS) - .putExtra(ANDROID_M_APP_PACKAGE, context.packageName) - .putExtra(ANDROID_M_APP_UID, context.applicationInfo.uid) - - startActivity(context, settingsIntent, null) - } - @SuppressLint("InlinedApi") private fun openSettingsOnAndroid8Plus() { val settingsIntent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/notifyme/NotifyMeViewModel.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/notifyme/NotifyMeViewModel.kt index cc7a5c0737bd..d6b7545a7443 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/notifyme/NotifyMeViewModel.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/notifyme/NotifyMeViewModel.kt @@ -26,7 +26,6 @@ import androidx.lifecycle.viewModelScope import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.CheckPermissionRationale import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.DismissComponent -import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.OpenSettings import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.OpenSettingsOnAndroid8Plus import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.ShowPermissionRationale import com.duckduckgo.common.ui.notifyme.NotifyMeViewModel.Command.UpdateNotificationsState @@ -58,7 +57,6 @@ class NotifyMeViewModel( object UpdateNotificationsState : Command() object UpdateNotificationsStateOnAndroid13Plus : Command() object OpenSettingsOnAndroid8Plus : Command() - object OpenSettings : Command() object CheckPermissionRationale : Command() object ShowPermissionRationale : Command() object DismissComponent : Command() @@ -130,11 +128,7 @@ class NotifyMeViewModel( } private fun openSettings() { - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.O) { - sendCommand(OpenSettingsOnAndroid8Plus) - } else { - sendCommand(OpenSettings) - } + sendCommand(OpenSettingsOnAndroid8Plus) } private fun isDismissed(): Boolean { diff --git a/common/common-ui/src/test/java/com/duckduckgo/common/ui/notifyme/NotifyMeViewModelTest.kt b/common/common-ui/src/test/java/com/duckduckgo/common/ui/notifyme/NotifyMeViewModelTest.kt index a834a34c0d97..90b7c7380c22 100644 --- a/common/common-ui/src/test/java/com/duckduckgo/common/ui/notifyme/NotifyMeViewModelTest.kt +++ b/common/common-ui/src/test/java/com/duckduckgo/common/ui/notifyme/NotifyMeViewModelTest.kt @@ -209,20 +209,6 @@ class NotifyMeViewModelTest { } } - @Test - fun whenOnNotifyMeButtonClickedOnAndroid6ThenOpenSettingsCommandIsSent() = runTest { - whenever(mockAppBuildConfig.sdkInt).thenReturn(Build.VERSION_CODES.M) - - testee.onNotifyMeButtonClicked() - - testee.commands().test { - assertEquals( - NotifyMeViewModel.Command.OpenSettings, - awaitItem(), - ) - } - } - @Test fun whenOnCloseButtonClickedThenCloseCommandIsSentAndSetDismissedIsCalled() = runTest { testee.onCloseButtonClicked() @@ -265,21 +251,6 @@ class NotifyMeViewModelTest { } } - @Test - fun whenHandleRequestPermissionRationaleOnAndroid6WithShouldShowRationaleFalseThenOpenSettingsCommandIsSent() = runTest { - whenever(mockAppBuildConfig.sdkInt).thenReturn(Build.VERSION_CODES.M) - val shouldShowRationale = false - - testee.handleRequestPermissionRationale(shouldShowRationale) - - testee.commands().test { - assertEquals( - NotifyMeViewModel.Command.OpenSettings, - awaitItem(), - ) - } - } - @Test fun whenOnResumeCalledForAndroid13PlusThenUpdateNotificationsStateOnAndroid13PlusCommandIsSent() = runTest { whenever(mockAppBuildConfig.sdkInt).thenReturn(Build.VERSION_CODES.TIRAMISU) diff --git a/common/common-utils/src/main/java/com/duckduckgo/common/utils/extensions/ActivityExtensions.kt b/common/common-utils/src/main/java/com/duckduckgo/common/utils/extensions/ActivityExtensions.kt index 445927e3f9cc..26c6abdd968f 100644 --- a/common/common-utils/src/main/java/com/duckduckgo/common/utils/extensions/ActivityExtensions.kt +++ b/common/common-utils/src/main/java/com/duckduckgo/common/utils/extensions/ActivityExtensions.kt @@ -19,7 +19,6 @@ package com.duckduckgo.common.utils.extensions import android.annotation.SuppressLint import android.content.Intent import android.net.Uri -import android.os.Build import android.provider.Settings import androidx.appcompat.app.AppCompatActivity @@ -39,12 +38,8 @@ fun AppCompatActivity.launchApplicationInfoSettings(): Boolean { } @SuppressLint("InlinedApi") -fun AppCompatActivity.launchAlwaysOnSystemSettings(sdkInt: Int) { - val intent = if (sdkInt >= Build.VERSION_CODES.N) { - Intent(Settings.ACTION_VPN_SETTINGS) - } else { - Intent("android.net.vpn.SETTINGS") - } +fun AppCompatActivity.launchAlwaysOnSystemSettings() { + val intent = Intent(Settings.ACTION_VPN_SETTINGS) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK startActivity(intent) } diff --git a/common/common-utils/src/main/java/com/duckduckgo/common/utils/extensions/StringExtensions.kt b/common/common-utils/src/main/java/com/duckduckgo/common/utils/extensions/StringExtensions.kt index 71d757f4c2eb..f7c96220cf63 100644 --- a/common/common-utils/src/main/java/com/duckduckgo/common/utils/extensions/StringExtensions.kt +++ b/common/common-utils/src/main/java/com/duckduckgo/common/utils/extensions/StringExtensions.kt @@ -19,7 +19,6 @@ package com.duckduckgo.common.utils.extensions import android.content.Context import android.graphics.drawable.Drawable import android.net.Uri -import android.os.Build import android.text.Html import android.text.Spanned import androidx.core.content.ContextCompat @@ -30,12 +29,8 @@ fun String.capitalizeFirstLetter() = this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } -@Suppress("deprecation") fun String.html(context: Context): Spanned { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return Html.fromHtml(this, Html.FROM_HTML_MODE_COMPACT, { htmlDrawable(context, it.toInt()) }, null) - } - return Html.fromHtml(this, { htmlDrawable(context, it.toInt()) }, null) + return Html.fromHtml(this, Html.FROM_HTML_MODE_COMPACT, { htmlDrawable(context, it.toInt()) }, null) } private fun htmlDrawable( diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementActivity.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementActivity.kt index 97647c73686b..88e468d27dde 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementActivity.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementActivity.kt @@ -308,7 +308,7 @@ class NetworkProtectionManagementActivity : DuckDuckGoActivity() { } private fun openVPNSettings() { - this.launchAlwaysOnSystemSettings(appBuildConfig.sdkInt) + this.launchAlwaysOnSystemSettings() } private fun resetToggle() { diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationBuilder.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationBuilder.kt index 44b6eb12f12e..2771a7b6f2d0 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationBuilder.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationBuilder.kt @@ -22,7 +22,6 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.TaskStackBuilder @@ -54,17 +53,15 @@ class RealNetPDisabledNotificationBuilder @Inject constructor( private val defaultDateTimeFormatter = SimpleDateFormat.getTimeInstance(DateFormat.SHORT) private fun registerChannel(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notificationManager = NotificationManagerCompat.from(context) - if (notificationManager.getNotificationChannel(NETP_ALERTS_CHANNEL_ID) == null) { - val channel = NotificationChannel( - NETP_ALERTS_CHANNEL_ID, - NETP_ALERTS_CHANNEL_NAME, - NotificationManager.IMPORTANCE_DEFAULT, - ) - channel.description = NETP_ALERTS_CHANNEL_DESCRIPTION - notificationManager.createNotificationChannel(channel) - } + val notificationManager = NotificationManagerCompat.from(context) + if (notificationManager.getNotificationChannel(NETP_ALERTS_CHANNEL_ID) == null) { + val channel = NotificationChannel( + NETP_ALERTS_CHANNEL_ID, + NETP_ALERTS_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT, + ) + channel.description = NETP_ALERTS_CHANNEL_DESCRIPTION + notificationManager.createNotificationChannel(channel) } } diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/settings/NetPVpnSettingsActivity.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/settings/NetPVpnSettingsActivity.kt index aed692dfdce1..36b8acb29785 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/settings/NetPVpnSettingsActivity.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/settings/NetPVpnSettingsActivity.kt @@ -121,7 +121,7 @@ class NetPVpnSettingsActivity : DuckDuckGoActivity() { } binding.alwaysOn.setOnClickListener { - this.launchAlwaysOnSystemSettings(appBuildConfig.sdkInt) + this.launchAlwaysOnSystemSettings() } binding.geoswitching.setOnClickListener { diff --git a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/NetPInternalSettingsActivity.kt b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/NetPInternalSettingsActivity.kt index b27455345eb1..f461af6ff722 100644 --- a/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/NetPInternalSettingsActivity.kt +++ b/network-protection/network-protection-internal/src/main/java/com/duckduckgo/networkprotection/internal/feature/NetPInternalSettingsActivity.kt @@ -20,7 +20,6 @@ import android.Manifest.permission.READ_PHONE_STATE import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.os.Build import android.os.Bundle import android.view.MenuItem import androidx.appcompat.widget.PopupMenu @@ -181,14 +180,10 @@ class NetPInternalSettingsActivity : DuckDuckGoActivity() { private fun setupConfigSection() { fun hasPhoneStatePermission(): Boolean { - return if (appBuildConfig.sdkInt >= Build.VERSION_CODES.M) { - ContextCompat.checkSelfPermission( - this, - READ_PHONE_STATE, - ) == PackageManager.PERMISSION_GRANTED - } else { - true - } + return ContextCompat.checkSelfPermission( + this, + READ_PHONE_STATE, + ) == PackageManager.PERMISSION_GRANTED } lifecycleScope.launch { diff --git a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/notification/NetpAccessRevokedNotificationBuilder.kt b/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/notification/NetpAccessRevokedNotificationBuilder.kt index 0ac7b51deca0..4940b917ebce 100644 --- a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/notification/NetpAccessRevokedNotificationBuilder.kt +++ b/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/notification/NetpAccessRevokedNotificationBuilder.kt @@ -22,7 +22,6 @@ import android.app.NotificationManager import android.app.PendingIntent import android.app.TaskStackBuilder import android.content.Context -import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.duckduckgo.browser.api.ui.BrowserScreens.SettingsScreenNoParams @@ -35,17 +34,15 @@ class NetpAccessRevokedNotificationBuilder @Inject constructor( private val globalActivityStarter: GlobalActivityStarter, ) { private fun registerChannel(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notificationManager = NotificationManagerCompat.from(context) - if (notificationManager.getNotificationChannel(RealNetPDisabledNotificationBuilder.NETP_ALERTS_CHANNEL_ID) == null) { - val channel = NotificationChannel( - RealNetPDisabledNotificationBuilder.NETP_ALERTS_CHANNEL_ID, - RealNetPDisabledNotificationBuilder.NETP_ALERTS_CHANNEL_NAME, - NotificationManager.IMPORTANCE_DEFAULT, - ) - channel.description = RealNetPDisabledNotificationBuilder.NETP_ALERTS_CHANNEL_DESCRIPTION - notificationManager.createNotificationChannel(channel) - } + val notificationManager = NotificationManagerCompat.from(context) + if (notificationManager.getNotificationChannel(RealNetPDisabledNotificationBuilder.NETP_ALERTS_CHANNEL_ID) == null) { + val channel = NotificationChannel( + RealNetPDisabledNotificationBuilder.NETP_ALERTS_CHANNEL_ID, + RealNetPDisabledNotificationBuilder.NETP_ALERTS_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT, + ) + channel.description = RealNetPDisabledNotificationBuilder.NETP_ALERTS_CHANNEL_DESCRIPTION + notificationManager.createNotificationChannel(channel) } } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewClient.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewClient.kt index fcf7250d5840..644c2188813f 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewClient.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewClient.kt @@ -19,11 +19,9 @@ package com.duckduckgo.subscriptions.impl.ui import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import androidx.annotation.RequiresApi import com.duckduckgo.app.browser.SpecialUrlDetector class SubscriptionsWebViewClient( @@ -31,7 +29,6 @@ class SubscriptionsWebViewClient( private val context: Context, ) : WebViewClient() { - @RequiresApi(Build.VERSION_CODES.N) override fun shouldOverrideUrlLoading( view: WebView, request: WebResourceRequest, From b27c834e400ebdcdcec6ef2de0fc5bb32d31b820 Mon Sep 17 00:00:00 2001 From: Josh Leibstein Date: Thu, 15 Feb 2024 13:12:39 +0000 Subject: [PATCH 07/19] Fix root bookmark import bug (#4166) --- .../bookmarks/model/SavedSitesParserTest.kt | 111 +++++++------ .../resources/bookmarks/bookmarks_brave.html | 152 +++++++++--------- .../resources/bookmarks/bookmarks_chrome.html | 0 .../bookmarks/bookmarks_ddg_android.html | 0 .../bookmarks/bookmarks_ddg_macos.html | 0 .../bookmarks/bookmarks_favorites_ddg.html | 0 .../bookmarks/bookmarks_firefox.html | 0 .../bookmarks/bookmarks_invalid.html | 0 .../resources/bookmarks/bookmarks_safari.html | 0 .../impl/service/SavedSitesParser.kt | 7 +- 10 files changed, 137 insertions(+), 133 deletions(-) rename app/src/{testInternal => test}/java/com/duckduckgo/app/bookmarks/model/SavedSitesParserTest.kt (76%) rename app/src/{testInternal => test}/resources/bookmarks/bookmarks_brave.html (99%) rename app/src/{testInternal => test}/resources/bookmarks/bookmarks_chrome.html (100%) rename app/src/{testInternal => test}/resources/bookmarks/bookmarks_ddg_android.html (100%) rename app/src/{testInternal => test}/resources/bookmarks/bookmarks_ddg_macos.html (100%) rename app/src/{testInternal => test}/resources/bookmarks/bookmarks_favorites_ddg.html (100%) rename app/src/{testInternal => test}/resources/bookmarks/bookmarks_firefox.html (100%) rename app/src/{testInternal => test}/resources/bookmarks/bookmarks_invalid.html (100%) rename app/src/{testInternal => test}/resources/bookmarks/bookmarks_safari.html (100%) diff --git a/app/src/testInternal/java/com/duckduckgo/app/bookmarks/model/SavedSitesParserTest.kt b/app/src/test/java/com/duckduckgo/app/bookmarks/model/SavedSitesParserTest.kt similarity index 76% rename from app/src/testInternal/java/com/duckduckgo/app/bookmarks/model/SavedSitesParserTest.kt rename to app/src/test/java/com/duckduckgo/app/bookmarks/model/SavedSitesParserTest.kt index c74d1c2fbb6e..9f6419b99e71 100644 --- a/app/src/testInternal/java/com/duckduckgo/app/bookmarks/model/SavedSitesParserTest.kt +++ b/app/src/test/java/com/duckduckgo/app/bookmarks/model/SavedSitesParserTest.kt @@ -40,9 +40,10 @@ import com.duckduckgo.savedsites.store.SavedSitesRelationsDao import com.duckduckgo.sync.crypto.EncryptResult import com.duckduckgo.sync.crypto.SyncLib import com.duckduckgo.sync.store.SyncStore +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue import kotlinx.coroutines.test.runTest import org.jsoup.Jsoup -import org.junit.Assert import org.junit.Before import org.junit.Rule import org.junit.Test @@ -103,14 +104,14 @@ class SavedSitesParserTest { @Test fun whenSomeBookmarksExistThenHtmlIsGenerated() = runTest { - val bookmark = SavedSite.Bookmark( + val bookmark = Bookmark( id = "bookmark1", title = "example", url = "www.example.com", SavedSitesNames.BOOKMARKS_ROOT, lastModified = "timestamp", ) - val favorite = SavedSite.Favorite(id = "fav1", title = "example", url = "www.example.com", lastModified = "timestamp", 0) + val favorite = Favorite(id = "fav1", title = "example", url = "www.example.com", lastModified = "timestamp", 0) val node = TreeNode(FolderTreeItem(SavedSitesNames.BOOKMARKS_ROOT, RealSavedSitesParser.BOOKMARKS_FOLDER, "", null, 0)) node.add(TreeNode(FolderTreeItem(bookmark.id, bookmark.title, bookmark.parentId, bookmark.url, 1))) @@ -134,7 +135,7 @@ class SavedSitesParserTest { "

\n" + "

\n" - Assert.assertEquals(expectedHtml, result) + assertEquals(expectedHtml, result) } @Test @@ -144,7 +145,7 @@ class SavedSitesParserTest { val result = parser.generateHtml(node, emptyList()) val expectedHtml = "" - Assert.assertEquals(expectedHtml, result) + assertEquals(expectedHtml, result) } @Test @@ -154,7 +155,7 @@ class SavedSitesParserTest { val bookmarks = parser.parseHtml(document, repository) - Assert.assertTrue(bookmarks.isEmpty()) + assertTrue(bookmarks.isEmpty()) } @Test @@ -164,15 +165,16 @@ class SavedSitesParserTest { val bookmarks = parser.parseHtml(document, repository).filterIsInstance() - Assert.assertEquals(17, bookmarks.size) + assertEquals(17, bookmarks.size) val firstBookmark = bookmarks.first() - Assert.assertEquals("https://support.mozilla.org/en-US/products/firefox", (firstBookmark as Bookmark).url) - Assert.assertEquals("Get Help", firstBookmark.title) + assertEquals("https://support.mozilla.org/en-US/products/firefox", firstBookmark.url) + assertEquals("Get Help", firstBookmark.title) val lastBookmark = bookmarks.last() - Assert.assertEquals("https://www.mozilla.org/en-US/firefox/central/", (lastBookmark as Bookmark).url) - Assert.assertEquals("Getting Started", lastBookmark.title) + assertEquals("https://www.mozilla.org/en-US/firefox/central/", lastBookmark.url) + assertEquals("Getting Started", lastBookmark.title) + assertEquals(SavedSitesNames.BOOKMARKS_ROOT, bookmarks[13].parentId) } @Test @@ -182,18 +184,18 @@ class SavedSitesParserTest { val bookmarks = parser.parseHtml(document, repository).filterIsInstance() - Assert.assertEquals(12, bookmarks.size) + assertEquals(12, bookmarks.size) val firstBookmark = bookmarks.first() - Assert.assertEquals( + assertEquals( "https://www.theguardian.com/international", - (firstBookmark as Bookmark).url, + firstBookmark.url, ) - Assert.assertEquals("News, sport and opinion from the Guardian's global edition | The Guardian", firstBookmark.title) + assertEquals("News, sport and opinion from the Guardian's global edition | The Guardian", firstBookmark.title) val lastBookmark = bookmarks.last() - Assert.assertEquals("https://www.macrumors.com/", (lastBookmark as Bookmark).url) - Assert.assertEquals("MacRumors: Apple News and Rumors", lastBookmark.title) + assertEquals("https://www.macrumors.com/", lastBookmark.url) + assertEquals("MacRumors: Apple News and Rumors", lastBookmark.title) } @Test @@ -203,18 +205,19 @@ class SavedSitesParserTest { val bookmarks = parser.parseHtml(document, repository).filterIsInstance() - Assert.assertEquals(12, bookmarks.size) + assertEquals(12, bookmarks.size) val firstBookmark = bookmarks.first() - Assert.assertEquals( + assertEquals( "https://www.theguardian.com/international", - (firstBookmark as Bookmark).url, + firstBookmark.url, ) - Assert.assertEquals("News, sport and opinion from the Guardian's global edition | The Guardian", firstBookmark.title) + assertEquals("News, sport and opinion from the Guardian's global edition | The Guardian", firstBookmark.title) val lastBookmark = bookmarks.last() - Assert.assertEquals("https://www.macrumors.com/", (lastBookmark as Bookmark).url) - Assert.assertEquals("MacRumors: Apple News and Rumors", lastBookmark.title) + assertEquals("https://www.macrumors.com/", lastBookmark.url) + assertEquals("MacRumors: Apple News and Rumors", lastBookmark.title) + assertEquals(SavedSitesNames.BOOKMARKS_ROOT, bookmarks[9].parentId) } @Test @@ -224,18 +227,19 @@ class SavedSitesParserTest { val bookmarks = parser.parseHtml(document, repository).filterNot { it is BookmarkFolder } - Assert.assertEquals(13, bookmarks.size) + assertEquals(13, bookmarks.size) val firstBookmark = bookmarks.first() - Assert.assertEquals( + assertEquals( "https://www.theguardian.com/international", (firstBookmark as Bookmark).url, ) - Assert.assertEquals("News, sport and opinion from the Guardian's global edition | The Guardian", firstBookmark.title) + assertEquals("News, sport and opinion from the Guardian's global edition | The Guardian", firstBookmark.title) val lastBookmark = bookmarks.last() - Assert.assertEquals("https://www.apple.com/uk/", (lastBookmark as Favorite).url) - Assert.assertEquals("Apple (United Kingdom)", lastBookmark.title) + assertEquals("https://www.apple.com/uk/", (lastBookmark as Favorite).url) + assertEquals("Apple (United Kingdom)", lastBookmark.title) + assertEquals(SavedSitesNames.BOOKMARKS_ROOT, (bookmarks[9] as Bookmark).parentId) } @Test @@ -245,18 +249,20 @@ class SavedSitesParserTest { val bookmarks = parser.parseHtml(document, repository).filterIsInstance() - Assert.assertEquals(13, bookmarks.size) + assertEquals(13, bookmarks.size) val firstBookmark = bookmarks.first() - Assert.assertEquals( + assertEquals( "https://www.theguardian.com/international", - (firstBookmark as Bookmark).url, + firstBookmark.url, ) - Assert.assertEquals("News, sport and opinion from the Guardian's global edition | The Guardian", firstBookmark.title) + assertEquals("News, sport and opinion from the Guardian's global edition | The Guardian", firstBookmark.title) val lastBookmark = bookmarks.last() - Assert.assertEquals("https://www.apple.com/uk/", (lastBookmark as Bookmark).url) - Assert.assertEquals("Apple (United Kingdom)", lastBookmark.title) + assertEquals("https://www.apple.com/uk/", lastBookmark.url) + assertEquals("Apple (United Kingdom)", lastBookmark.title) + assertEquals(SavedSitesNames.BOOKMARKS_ROOT, bookmarks[9].parentId) + assertEquals(SavedSitesNames.BOOKMARKS_ROOT, bookmarks[12].parentId) } @Test @@ -266,18 +272,19 @@ class SavedSitesParserTest { val bookmarks = parser.parseHtml(document, repository).filterIsInstance() - Assert.assertEquals(14, bookmarks.size) + assertEquals(14, bookmarks.size) val firstBookmark = bookmarks.first() - Assert.assertEquals( + assertEquals( "https://www.apple.com/uk", - (firstBookmark as Bookmark).url, + firstBookmark.url, ) - Assert.assertEquals("Apple", firstBookmark.title) + assertEquals("Apple", firstBookmark.title) val lastBookmark = bookmarks.last() - Assert.assertEquals("https://www.macrumors.com/", (lastBookmark as Bookmark).url) - Assert.assertEquals("MacRumors: Apple News and Rumors", lastBookmark.title) + assertEquals("https://www.macrumors.com/", lastBookmark.url) + assertEquals("MacRumors: Apple News and Rumors", lastBookmark.title) + assertEquals(SavedSitesNames.BOOKMARKS_ROOT, bookmarks[11].parentId) } @Test @@ -287,12 +294,12 @@ class SavedSitesParserTest { val savedSites = parser.parseHtml(document, repository) - val favorites = savedSites.filterIsInstance() - val bookmarks = savedSites.filterIsInstance() + val favorites = savedSites.filterIsInstance() + val bookmarks = savedSites.filterIsInstance() - Assert.assertEquals(12, savedSites.size) - Assert.assertEquals(3, favorites.size) - Assert.assertEquals(9, bookmarks.size) + assertEquals(12, savedSites.size) + assertEquals(3, favorites.size) + assertEquals(9, bookmarks.size) } @Test @@ -333,11 +340,11 @@ class SavedSitesParserTest { val link = linkItem.attr("href") val title = linkItem.text() if (inFavorite) { - savedSites.add(SavedSite.Favorite("favorite1", title = title, url = link, "timestamp", favorites)) + savedSites.add(Favorite("favorite1", title = title, url = link, "timestamp", favorites)) favorites++ } else { savedSites.add( - SavedSite.Bookmark( + Bookmark( "bookmark1", title = title, url = link, @@ -350,11 +357,11 @@ class SavedSitesParserTest { } } - val favoritesLists = savedSites.filterIsInstance() - val bookmarks = savedSites.filterIsInstance() + val favoritesLists = savedSites.filterIsInstance() + val bookmarks = savedSites.filterIsInstance() - Assert.assertEquals(12, savedSites.size) - Assert.assertEquals(3, favoritesLists.size) - Assert.assertEquals(9, bookmarks.size) + assertEquals(12, savedSites.size) + assertEquals(3, favoritesLists.size) + assertEquals(9, bookmarks.size) } } diff --git a/app/src/testInternal/resources/bookmarks/bookmarks_brave.html b/app/src/test/resources/bookmarks/bookmarks_brave.html similarity index 99% rename from app/src/testInternal/resources/bookmarks/bookmarks_brave.html rename to app/src/test/resources/bookmarks/bookmarks_brave.html index 04808720b39b..0b9451a38798 100644 --- a/app/src/testInternal/resources/bookmarks/bookmarks_brave.html +++ b/app/src/test/resources/bookmarks/bookmarks_brave.html @@ -1,76 +1,76 @@ - - - - - -Bookmarks -

Bookmarks

-

-

Bookmarks bar

-

-

FolderA-Level1

-

-

FolderA-Level2

-

-

FolderA-Level3

-

-

News, sport and opinion from the Guardian's global edition | The Guardian -

-

Digg - What the Internet is talking about right now -

-

Wikipedia -

-

FolderB-Level1

-

-

FolderB-Level2

-

-

FolderB-Level3-a

-

-

Bloomberg.com -

-

FolderB-Level3-b

-

-

TechCrunch – Startup and Technology News -

-

The Verge -

-

Techmeme -

-

EmptyFolder

-

-

-

DuplicateFolderName

-

-

Breaking News | Irish & International Headlines | The Irish Times -

-

DuplicateFolderName

-

-

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video -

-

DuckDuckGo — Privacy, simplified. -

DupeFolderNameContents

-

-

MacRumors: Apple News and Rumors -

-

DupeFolderNameContents

-

-

MacRumors: Apple News and Rumors -

-

-

+ + + + + +Bookmarks +

Bookmarks

+

+

Bookmarks bar

+

+

FolderA-Level1

+

+

FolderA-Level2

+

+

FolderA-Level3

+

+

News, sport and opinion from the Guardian's global edition | The Guardian +

+

Digg - What the Internet is talking about right now +

+

Wikipedia +

+

FolderB-Level1

+

+

FolderB-Level2

+

+

FolderB-Level3-a

+

+

Bloomberg.com +

+

FolderB-Level3-b

+

+

TechCrunch – Startup and Technology News +

+

The Verge +

+

Techmeme +

+

EmptyFolder

+

+

+

DuplicateFolderName

+

+

Breaking News | Irish & International Headlines | The Irish Times +

+

DuplicateFolderName

+

+

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video +

+

DuckDuckGo — Privacy, simplified. +

DupeFolderNameContents

+

+

MacRumors: Apple News and Rumors +

+

DupeFolderNameContents

+

+

MacRumors: Apple News and Rumors +

+

+

diff --git a/app/src/testInternal/resources/bookmarks/bookmarks_chrome.html b/app/src/test/resources/bookmarks/bookmarks_chrome.html similarity index 100% rename from app/src/testInternal/resources/bookmarks/bookmarks_chrome.html rename to app/src/test/resources/bookmarks/bookmarks_chrome.html diff --git a/app/src/testInternal/resources/bookmarks/bookmarks_ddg_android.html b/app/src/test/resources/bookmarks/bookmarks_ddg_android.html similarity index 100% rename from app/src/testInternal/resources/bookmarks/bookmarks_ddg_android.html rename to app/src/test/resources/bookmarks/bookmarks_ddg_android.html diff --git a/app/src/testInternal/resources/bookmarks/bookmarks_ddg_macos.html b/app/src/test/resources/bookmarks/bookmarks_ddg_macos.html similarity index 100% rename from app/src/testInternal/resources/bookmarks/bookmarks_ddg_macos.html rename to app/src/test/resources/bookmarks/bookmarks_ddg_macos.html diff --git a/app/src/testInternal/resources/bookmarks/bookmarks_favorites_ddg.html b/app/src/test/resources/bookmarks/bookmarks_favorites_ddg.html similarity index 100% rename from app/src/testInternal/resources/bookmarks/bookmarks_favorites_ddg.html rename to app/src/test/resources/bookmarks/bookmarks_favorites_ddg.html diff --git a/app/src/testInternal/resources/bookmarks/bookmarks_firefox.html b/app/src/test/resources/bookmarks/bookmarks_firefox.html similarity index 100% rename from app/src/testInternal/resources/bookmarks/bookmarks_firefox.html rename to app/src/test/resources/bookmarks/bookmarks_firefox.html diff --git a/app/src/testInternal/resources/bookmarks/bookmarks_invalid.html b/app/src/test/resources/bookmarks/bookmarks_invalid.html similarity index 100% rename from app/src/testInternal/resources/bookmarks/bookmarks_invalid.html rename to app/src/test/resources/bookmarks/bookmarks_invalid.html diff --git a/app/src/testInternal/resources/bookmarks/bookmarks_safari.html b/app/src/test/resources/bookmarks/bookmarks_safari.html similarity index 100% rename from app/src/testInternal/resources/bookmarks/bookmarks_safari.html rename to app/src/test/resources/bookmarks/bookmarks_safari.html diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/service/SavedSitesParser.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/service/SavedSitesParser.kt index e56e3414c11a..6a5781fb1aaf 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/service/SavedSitesParser.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/service/SavedSitesParser.kt @@ -136,7 +136,7 @@ class RealSavedSitesParser : SavedSitesParser { if (children.size > 1) { rootElement = Element("DL").appendChildren(children) } - return parseElement(rootElement, "", savedSitesRepository, mutableListOf(), false) + return parseElement(rootElement, SavedSitesNames.BOOKMARKS_ROOT, savedSitesRepository, mutableListOf(), false) } private fun parseElement( @@ -163,12 +163,9 @@ class RealSavedSitesParser : SavedSitesParser { if (isFavoritesFolder(folderName) || isBookmarksFolder(folderName)) { parseElement(element, SavedSitesNames.BOOKMARKS_ROOT, savedSitesRepository, savedSites, folderName == FAVORITES_FOLDER) } else { - val folderParentId = parentId.ifEmpty { - SavedSitesNames.BOOKMARKS_ROOT - } val bookmarkFolder = BookmarkFolder( name = folderName, - parentId = folderParentId, + parentId = parentId, lastModified = DatabaseDateFormatter.iso8601(), deleted = null, ) From 1569e242592484ac2a955075956c3bfae1a3cd7b Mon Sep 17 00:00:00 2001 From: Aitor Viana Date: Mon, 19 Feb 2024 13:21:53 +0000 Subject: [PATCH 08/19] Cancel previous always on monitor on VPN start Task/Issue URL: https://app.asana.com/0/488551667048375/1206608309382825/f ### Description Cancel always on previous monitor on VPN start ### Steps to test this PR _Test_ - [x] install from this branch and enable AppTP - [x] verify`state: VpnServiceStateStats(...)` logcat prints periodically, the frequency should decrease - [x] add an app to the exclusion list to force a VPN re-configuration - [x] verify`state: VpnServiceStateStats(...)` logcat prints periodically, at the initial frequency --- .../mobile/android/vpn/service/TrackerBlockingVpnService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt index 10e9976f274e..a9e4808ebdff 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/service/TrackerBlockingVpnService.kt @@ -298,6 +298,8 @@ class TrackerBlockingVpnService : VpnService(), CoroutineScope by MainScope(), V logcat { "VPN log: NEW network ${vpnNetworkStack.name}" } } dnsChangeCallback.unregister() + // cancel previous monitor + alwaysOnStateJob.cancel() vpnServiceStateStatsDao.insert(createVpnState(state = ENABLING)) From 47708ddd41011e78fd45cdc24ace3da1b124aa6b Mon Sep 17 00:00:00 2001 From: Josh Leibstein Date: Mon, 19 Feb 2024 14:29:59 +0000 Subject: [PATCH 09/19] Extract applink logic from BrowserTabFragment (#4183) --- .../app/browser/BrowserTabFragment.kt | 99 +++---------------- .../app/browser/applinks/AppLinksLauncher.kt | 84 ++++++++++++++++ .../applinks/AppLinksSnackBarConfigurator.kt | 96 ++++++++++++++++++ 3 files changed, 191 insertions(+), 88 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksLauncher.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksSnackBarConfigurator.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index be224f3cf465..a53211489862 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -23,7 +23,6 @@ import android.app.ActivityOptions import android.app.PendingIntent import android.content.* import android.content.pm.ActivityInfo -import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.content.res.Configuration @@ -103,6 +102,8 @@ import com.duckduckgo.app.browser.BrowserTabViewModel.SavedSiteChangedViewState import com.duckduckgo.app.browser.R.string import com.duckduckgo.app.browser.WebViewErrorResponse.LOADING import com.duckduckgo.app.browser.WebViewErrorResponse.OMITTED +import com.duckduckgo.app.browser.applinks.AppLinksLauncher +import com.duckduckgo.app.browser.applinks.AppLinksSnackBarConfigurator import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager import com.duckduckgo.app.browser.databinding.ContentSiteLocationPermissionDialogBinding @@ -458,6 +459,12 @@ class BrowserTabFragment : @Inject lateinit var privacyProtectionsPopupFactory: PrivacyProtectionsPopupFactory + @Inject + lateinit var appLinksSnackBarConfigurator: AppLinksSnackBarConfigurator + + @Inject + lateinit var appLinksLauncher: AppLinksLauncher + /** * We use this to monitor whether the user was seeing the in-context Email Protection signup prompt * This is needed because the activity stack will be cleared if an external link is opened in our browser @@ -1614,84 +1621,12 @@ class BrowserTabFragment : } private fun showAppLinkSnackBar(appLink: SpecialUrlDetector.UrlType.AppLink) { - view?.let { view -> - - val message: String? - val action: String? - - if (appLink.appIntent != null) { - val packageName = appLink.appIntent!!.component?.packageName ?: return - message = getString(R.string.appLinkSnackBarMessage, getAppName(packageName)) - action = getString(R.string.appLinkSnackBarAction) - } else { - message = getString(R.string.appLinkMultipleSnackBarMessage) - action = getString(R.string.appLinkMultipleSnackBarAction) - } - - appLinksSnackBar = view.makeSnackbarWithNoBottomInset( - message, - Snackbar.LENGTH_LONG, - ) - .setAction(action) { - pixel.fire(AppPixelName.APP_LINKS_SNACKBAR_OPEN_ACTION_PRESSED) - openAppLink(appLink) - } - .addCallback( - object : BaseTransientBottomBar.BaseCallback() { - override fun onShown(transientBottomBar: Snackbar?) { - super.onShown(transientBottomBar) - pixel.fire(AppPixelName.APP_LINKS_SNACKBAR_SHOWN) - } - - override fun onDismissed( - transientBottomBar: Snackbar?, - event: Int, - ) { - super.onDismissed(transientBottomBar, event) - } - }, - ) - - appLinksSnackBar?.setDuration(6000)?.show() - } - } - - private fun getAppName(packageName: String): String? { - val packageManager: PackageManager? = context?.packageManager - val applicationInfo: ApplicationInfo? = try { - packageManager?.getApplicationInfo(packageName, 0) - } catch (e: PackageManager.NameNotFoundException) { - null - } - return if (applicationInfo != null) { - packageManager?.getApplicationLabel(applicationInfo).toString() - } else { - null - } + appLinksSnackBar = appLinksSnackBarConfigurator.configureAppLinkSnackBar(view = view, appLink = appLink, viewModel = viewModel) + appLinksSnackBar?.show() } private fun openAppLink(appLink: SpecialUrlDetector.UrlType.AppLink) { - if (appLink.appIntent != null) { - appLink.appIntent!!.flags = Intent.FLAG_ACTIVITY_NEW_TASK - try { - startActivityOrQuietlyFail(appLink.appIntent!!) - } catch (e: SecurityException) { - showToast(R.string.unableToOpenLink) - } - } else if (appLink.excludedComponents != null) { - val title = getString(R.string.appLinkIntentChooserTitle) - val chooserIntent = getChooserIntent(appLink.uriString, title, appLink.excludedComponents!!) - startActivityOrQuietlyFail(chooserIntent) - } - viewModel.clearPreviousUrl() - } - - private fun startActivityOrQuietlyFail(intent: Intent) { - try { - startActivity(intent) - } catch (e: ActivityNotFoundException) { - Timber.w(e, "Activity not found") - } + appLinksLauncher.openAppLink(context = context, appLink = appLink, viewModel = viewModel) } private fun dismissAppLinkSnackBar() { @@ -1699,18 +1634,6 @@ class BrowserTabFragment : appLinksSnackBar = null } - private fun getChooserIntent( - url: String?, - title: String, - excludedComponents: List, - ): Intent { - val urlIntent = Intent.parseUri(url, Intent.URI_ANDROID_APP_SCHEME) - urlIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - val chooserIntent = Intent.createChooser(urlIntent, title) - chooserIntent.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, excludedComponents.toTypedArray()) - return chooserIntent - } - private fun openExternalDialog( intent: Intent, fallbackUrl: String? = null, diff --git a/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksLauncher.kt b/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksLauncher.kt new file mode 100644 index 000000000000..c0cb7b014920 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksLauncher.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 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.app.browser.applinks + +import android.content.ActivityNotFoundException +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Build +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.annotation.StringRes +import com.duckduckgo.app.browser.BrowserTabViewModel +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.AppLink +import com.duckduckgo.appbuildconfig.api.AppBuildConfig +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import timber.log.Timber + +interface AppLinksLauncher { + fun openAppLink(context: Context?, appLink: AppLink, viewModel: BrowserTabViewModel) +} + +@ContributesBinding(AppScope::class) +class DuckDuckGoAppLinksLauncher @Inject constructor( + private val appBuildConfig: AppBuildConfig, +) : AppLinksLauncher { + + @Suppress("NewApi") + override fun openAppLink(context: Context?, appLink: AppLink, viewModel: BrowserTabViewModel) { + if (context == null) return + appLink.appIntent?.let { + it.flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivityOrQuietlyFail(context, it) + } ?: run { + if (appLink.excludedComponents != null && appBuildConfig.sdkInt >= Build.VERSION_CODES.N) { + val title = context.getString(R.string.appLinkIntentChooserTitle) + val chooserIntent = getChooserIntent(appLink.uriString, title, appLink.excludedComponents!!) + startActivityOrQuietlyFail(context, chooserIntent) + } + } + viewModel.clearPreviousUrl() + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun getChooserIntent(url: String?, title: String, excludedComponents: List): Intent { + val urlIntent = Intent.parseUri(url, Intent.URI_ANDROID_APP_SCHEME).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + return Intent.createChooser(urlIntent, title).apply { + putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, excludedComponents.toTypedArray()) + } + } + + private fun startActivityOrQuietlyFail(context: Context, intent: Intent) { + try { + context.startActivity(intent) + } catch (exception: ActivityNotFoundException) { + Timber.e(exception, "Activity not found") + } catch (exception: SecurityException) { + showToast(context, R.string.unableToOpenLink) + } + } + + private fun showToast(context: Context, @StringRes messageId: Int, length: Int = Toast.LENGTH_LONG) { + Toast.makeText(context.applicationContext, messageId, length).show() + } +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksSnackBarConfigurator.kt b/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksSnackBarConfigurator.kt new file mode 100644 index 000000000000..5662b6c13b54 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksSnackBarConfigurator.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 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.app.browser.applinks + +import android.content.Context +import android.content.pm.PackageManager +import android.view.View +import com.duckduckgo.app.browser.BrowserTabViewModel +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.AppLink +import com.duckduckgo.app.pixels.AppPixelName +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.common.ui.view.makeSnackbarWithNoBottomInset +import com.duckduckgo.di.scopes.AppScope +import com.google.android.material.snackbar.BaseTransientBottomBar +import com.google.android.material.snackbar.Snackbar +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import timber.log.Timber + +interface AppLinksSnackBarConfigurator { + fun configureAppLinkSnackBar(view: View?, appLink: AppLink, viewModel: BrowserTabViewModel): Snackbar? +} + +@ContributesBinding(AppScope::class) +class DuckDuckGoAppLinksSnackBarConfigurator @Inject constructor( + private val appLinksLauncher: AppLinksLauncher, + private val pixel: Pixel, +) : AppLinksSnackBarConfigurator { + + override fun configureAppLinkSnackBar(view: View?, appLink: AppLink, viewModel: BrowserTabViewModel): Snackbar? { + return view?.let { + val context = it.context + val (message, action) = getSnackBarContent(context, appLink) ?: return null + + it.makeSnackbarWithNoBottomInset(message, Snackbar.LENGTH_LONG).apply { + setAction(action) { + pixel.fire(AppPixelName.APP_LINKS_SNACKBAR_OPEN_ACTION_PRESSED) + appLinksLauncher.openAppLink(context, appLink, viewModel) + } + addCallback( + object : BaseTransientBottomBar.BaseCallback() { + override fun onShown(transientBottomBar: Snackbar?) { + super.onShown(transientBottomBar) + pixel.fire(AppPixelName.APP_LINKS_SNACKBAR_SHOWN) + } + }, + ) + duration = DURATION + } + } + } + + private fun getSnackBarContent(context: Context, appLink: AppLink): Pair? { + val appIntent = appLink.appIntent + return if (appIntent != null) { + val packageName = appIntent.component?.packageName ?: return null + val message = context.getString(R.string.appLinkSnackBarMessage, getAppName(context, packageName)) + val action = context.getString(R.string.appLinkSnackBarAction) + message to action + } else { + val message = context.getString(R.string.appLinkMultipleSnackBarMessage) + val action = context.getString(R.string.appLinkMultipleSnackBarAction) + message to action + } + } + + private fun getAppName(context: Context, packageName: String): String? { + return try { + val packageManager = context.packageManager + val applicationInfo = packageManager.getApplicationInfo(packageName, 0) + packageManager.getApplicationLabel(applicationInfo).toString() + } catch (exception: PackageManager.NameNotFoundException) { + Timber.e(exception, "App name not found") + null + } + } + + companion object { + const val DURATION = 6000 + } +} From 23d248737c29091c415e3db55b42fb08f3aa7ed4 Mon Sep 17 00:00:00 2001 From: Aitor Viana Date: Mon, 19 Feb 2024 16:31:20 +0000 Subject: [PATCH 10/19] load privacy plugins data into memory just for main process (#4189) Task/Issue URL: https://app.asana.com/0/488551667048375/1206608309382832/f ### Description Many privacy plugins data is loaded into memory for both `:app` and `:vpn` processes. Even when the plugins are only used from the app process, because privacy config is only downloaded from the `main` process, the loading mistakenly happens in the `vpn` process as well because all plugins have a `loadToMemory` method that is executed in the `init` block (ie. when the instance is created) and the `PluginPoint` is a dependency to the `Application` instance, which is created for both `vpn` and `app` processes. However loading into data for the `vpn` process should not be required/done as we don't use that data from the `vpn` process. This task is a stop gap to avoid that in-memory data loading to happen. In sub-sequent PRs we'll want to have a better nad more standardize mechanism to perform in-memory Cache backed by disk storage. ### Steps to test this PR Code review and smoke test the app Make sure privacy config downloads and sets correctly. --- .../adclick/impl/di/AdClickModule.kt | 4 ++- .../store/AdClickAttributionRepository.kt | 5 +++- .../RealAdClickAttributionRepositoryTest.kt | 1 + .../store/MediaPlaybackRepository.kt | 6 +++- .../duckduckgo/app/di/ApplicationModule.kt | 7 +++++ .../store/RealMediaPlaybackRepositoryTest.kt | 1 + .../autoconsent/impl/di/AutoconsentModule.kt | 4 ++- .../AutoconsentExceptionsRepository.kt | 7 ++++- ...RealAutoconsentExceptionsRepositoryTest.kt | 6 ++-- .../autofill/impl/di/AutofillModule.kt | 7 +++-- .../feature/AutofillFeatureRepository.kt | 7 ++++- ...ailProtectionInContextFeatureRepository.kt | 7 ++++- .../cookies/impl/di/CookiesModule.kt | 7 +++-- .../cookies/store/CookiesRepository.kt | 5 +++- .../ContentScopeScriptsCookieRepository.kt | 9 ++++-- .../cookies/store/RealCookieRepositoryTest.kt | 4 +++ .../com/duckduckgo/app/di/IsMainProcess.kt | 23 +++++++++++++++ .../impl/di/ElementHidingModule.kt | 4 ++- .../store/ElementHidingRepository.kt | 5 +++- .../store/ElementHidingRepositoryTest.kt | 3 ++ .../impl/di/FingerprintProtectionModule.kt | 16 +++++++---- .../FingerprintingBatteryRepository.kt | 5 +++- .../FingerprintingCanvasRepository.kt | 7 +++-- .../FingerprintingHardwareRepository.kt | 5 +++- .../FingerprintingScreenSizeRepository.kt | 5 +++- ...ingerprintingTemporaryStorageRepository.kt | 5 +++- ...RealFingerprintingBatteryRepositoryTest.kt | 3 ++ .../RealFingerprintingCanvasRepositoryTest.kt | 3 ++ ...ealFingerprintingHardwareRepositoryTest.kt | 3 ++ ...lFingerprintingScreenSizeRepositoryTest.kt | 3 ++ ...rprintingTemporaryStorageRepositoryTest.kt | 3 ++ .../config/impl/di/PrivacyConfigModule.kt | 28 +++++++++++++------ .../impl/RealPrivacyConfigPersisterTest.kt | 1 + .../config/impl/ReferenceTestUtilities.kt | 12 ++++---- .../features/amplinks/AmpLinksRepository.kt | 5 +++- .../ContentBlockingRepository.kt | 7 ++++- .../store/features/drm/DrmRepository.kt | 5 +++- .../store/features/gpc/GpcRepository.kt | 7 ++++- .../store/features/https/HttpsRepository.kt | 7 ++++- .../TrackerAllowlistRepository.kt | 7 ++++- .../TrackingParametersRepository.kt | 5 +++- .../UnprotectedTemporaryRepository.kt | 7 ++++- .../features/useragent/UserAgentRepository.kt | 7 ++++- .../amplinks/RealAmpLinksRepositoryTest.kt | 4 +++ .../RealContentBlockingRepositoryTest.kt | 3 ++ .../features/drm/RealDrmRepositoryTest.kt | 6 ++-- .../features/gpc/RealGpcRepositoryTest.kt | 6 ++++ .../features/https/RealHttpsRepositoryTest.kt | 4 +++ .../RealTrackerAllowlistRepositoryTest.kt | 4 +++ .../RealTrackingParametersRepositoryTest.kt | 4 +++ .../RealUnprotectedTemporaryRepositoryTest.kt | 4 +++ .../useragent/RealUserAgentRepositoryTest.kt | 6 ++++ .../filterer/impl/di/RequestFiltererModule.kt | 4 ++- .../store/RequestFiltererRepository.kt | 5 +++- .../RealRequestFiltererRepositoryTest.kt | 4 +++ .../impl/di/RuntimeChecksModule.kt | 4 ++- .../store/RuntimeChecksRepository.kt | 9 ++++-- .../store/RuntimeChecksRepositoryTest.kt | 3 ++ .../impl/drmblock/DrmBlockRepository.kt | 6 +++- .../voice/impl/di/VoiceSearchModule.kt | 4 ++- .../VoiceSearchFeatureRepository.kt | 7 ++++- 61 files changed, 298 insertions(+), 67 deletions(-) create mode 100644 di/src/main/java/com/duckduckgo/app/di/IsMainProcess.kt diff --git a/ad-click/ad-click-impl/src/main/java/com/duckduckgo/adclick/impl/di/AdClickModule.kt b/ad-click/ad-click-impl/src/main/java/com/duckduckgo/adclick/impl/di/AdClickModule.kt index 2b878d930fa5..2b2e93ad63d1 100644 --- a/ad-click/ad-click-impl/src/main/java/com/duckduckgo/adclick/impl/di/AdClickModule.kt +++ b/ad-click/ad-click-impl/src/main/java/com/duckduckgo/adclick/impl/di/AdClickModule.kt @@ -24,6 +24,7 @@ import com.duckduckgo.adclick.store.AdClickDatabase.Companion.ALL_MIGRATIONS import com.duckduckgo.adclick.store.AdClickFeatureToggleRepository import com.duckduckgo.adclick.store.RealAdClickAttributionRepository import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesTo @@ -51,8 +52,9 @@ class AdClickModule { database: AdClickDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): AdClickAttributionRepository { - return RealAdClickAttributionRepository(database, appCoroutineScope, dispatcherProvider) + return RealAdClickAttributionRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) diff --git a/ad-click/ad-click-store/src/main/java/com/duckduckgo/adclick/store/AdClickAttributionRepository.kt b/ad-click/ad-click-store/src/main/java/com/duckduckgo/adclick/store/AdClickAttributionRepository.kt index 3fb54d3f3916..78f6ad1efa0c 100644 --- a/ad-click/ad-click-store/src/main/java/com/duckduckgo/adclick/store/AdClickAttributionRepository.kt +++ b/ad-click/ad-click-store/src/main/java/com/duckduckgo/adclick/store/AdClickAttributionRepository.kt @@ -38,6 +38,7 @@ class RealAdClickAttributionRepository( val database: AdClickDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : AdClickAttributionRepository { private val adClickAttributionDao: AdClickDao = database.adClickDao() @@ -49,7 +50,9 @@ class RealAdClickAttributionRepository( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/ad-click/ad-click-store/src/test/java/com/duckduckgo/adclick/store/RealAdClickAttributionRepositoryTest.kt b/ad-click/ad-click-store/src/test/java/com/duckduckgo/adclick/store/RealAdClickAttributionRepositoryTest.kt index 5424c01b9aa8..c89569f5151d 100644 --- a/ad-click/ad-click-store/src/test/java/com/duckduckgo/adclick/store/RealAdClickAttributionRepositoryTest.kt +++ b/ad-click/ad-click-store/src/test/java/com/duckduckgo/adclick/store/RealAdClickAttributionRepositoryTest.kt @@ -48,6 +48,7 @@ class RealAdClickAttributionRepositoryTest { database = mockDatabase, coroutineScope = TestScope(), dispatcherProvider = coroutineRule.testDispatcherProvider, + isMainProcess = true, ) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/mediaplayback/store/MediaPlaybackRepository.kt b/app/src/main/java/com/duckduckgo/app/browser/mediaplayback/store/MediaPlaybackRepository.kt index e0348ca4fc77..829a799b1e17 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/mediaplayback/store/MediaPlaybackRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/mediaplayback/store/MediaPlaybackRepository.kt @@ -17,6 +17,7 @@ package com.duckduckgo.app.browser.mediaplayback.store import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.feature.toggles.api.FeatureExceptions @@ -38,13 +39,16 @@ class RealMediaPlaybackRepository @Inject constructor( private val mediaPlaybackDao: MediaPlaybackDao, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ) : MediaPlaybackRepository { override val exceptions = CopyOnWriteArrayList() init { appCoroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/app/src/main/java/com/duckduckgo/app/di/ApplicationModule.kt b/app/src/main/java/com/duckduckgo/app/di/ApplicationModule.kt index 8ae86d207d6c..8e0fe39c7629 100644 --- a/app/src/main/java/com/duckduckgo/app/di/ApplicationModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/ApplicationModule.kt @@ -51,4 +51,11 @@ object ProcessNameModule { "main" } } + + @SingleInstanceIn(AppScope::class) + @Provides + @IsMainProcess + fun providerIsMainProcess(@ProcessName processName: String): Boolean { + return processName == "main" + } } diff --git a/app/src/test/java/com/duckduckgo/app/browser/mediaplayback/store/RealMediaPlaybackRepositoryTest.kt b/app/src/test/java/com/duckduckgo/app/browser/mediaplayback/store/RealMediaPlaybackRepositoryTest.kt index 584a2da4dbee..ef75ef8172bc 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/mediaplayback/store/RealMediaPlaybackRepositoryTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/mediaplayback/store/RealMediaPlaybackRepositoryTest.kt @@ -65,6 +65,7 @@ class RealMediaPlaybackRepositoryTest { mockMediaPlaybackDao, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) } diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/di/AutoconsentModule.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/di/AutoconsentModule.kt index aaa71c099899..02f9712a406b 100644 --- a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/di/AutoconsentModule.kt +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/di/AutoconsentModule.kt @@ -19,6 +19,7 @@ package com.duckduckgo.autoconsent.impl.di import android.content.Context import androidx.room.Room import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentExceptionsRepository import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeature import com.duckduckgo.autoconsent.impl.remoteconfig.AutoconsentFeatureSettingsRepository @@ -58,8 +59,9 @@ object AutoconsentModule { database: AutoconsentDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): AutoconsentExceptionsRepository { - return RealAutoconsentExceptionsRepository(appCoroutineScope, dispatcherProvider, database) + return RealAutoconsentExceptionsRepository(appCoroutineScope, dispatcherProvider, database, isMainProcess) } @SingleInstanceIn(AppScope::class) diff --git a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsRepository.kt b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsRepository.kt index 53f0f09fd7b2..b629c03b0c67 100644 --- a/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsRepository.kt +++ b/autoconsent/autoconsent-impl/src/main/java/com/duckduckgo/autoconsent/impl/remoteconfig/AutoconsentExceptionsRepository.kt @@ -33,13 +33,18 @@ class RealAutoconsentExceptionsRepository( coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, val database: AutoconsentDatabase, + isMainProcess: Boolean, ) : AutoconsentExceptionsRepository { private val dao = database.autoconsentDao() override val exceptions = CopyOnWriteArrayList() init { - coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } } override fun insertAllExceptions(exceptions: List) { diff --git a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentExceptionsRepositoryTest.kt b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentExceptionsRepositoryTest.kt index 75bdd896425c..976e96ad2f25 100644 --- a/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentExceptionsRepositoryTest.kt +++ b/autoconsent/autoconsent-impl/src/test/java/com/duckduckgo/autoconsent/impl/remoteconfig/RealAutoconsentExceptionsRepositoryTest.kt @@ -52,14 +52,14 @@ class RealAutoconsentExceptionsRepositoryTest { fun whenRepositoryIsCreatedThenExceptionsLoadedIntoMemory() { givenDaoContainsExceptions() - repository = RealAutoconsentExceptionsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase) + repository = RealAutoconsentExceptionsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase, isMainProcess = true) assertEquals(exception.toFeatureException(), repository.exceptions.first()) } @Test fun whenUpdateAllThenUpdateAllCalled() = runTest { - repository = RealAutoconsentExceptionsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase) + repository = RealAutoconsentExceptionsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase, isMainProcess = true) repository.insertAllExceptions(listOf()) @@ -69,7 +69,7 @@ class RealAutoconsentExceptionsRepositoryTest { @Test fun whenUpdateAllThenPreviousExceptionsAreCleared() = runTest { givenDaoContainsExceptions() - repository = RealAutoconsentExceptionsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase) + repository = RealAutoconsentExceptionsRepository(TestScope(), coroutineRule.testDispatcherProvider, mockDatabase, isMainProcess = true) assertEquals(1, repository.exceptions.size) reset(mockDao) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt index 65a97f35b3b9..21736e5b18e8 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.room.Room import com.duckduckgo.anvil.annotations.ContributesPluginPoint import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin import com.duckduckgo.autofill.api.InternalTestUserChecker @@ -112,8 +113,9 @@ class AutofillModule { database: AutofillDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): AutofillFeatureRepository { - return RealAutofillFeatureRepository(database, appCoroutineScope, dispatcherProvider) + return RealAutofillFeatureRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -122,8 +124,9 @@ class AutofillModule { database: EmailProtectionInContextDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): EmailProtectionInContextFeatureRepository { - return RealEmailProtectionInContextFeatureRepository(database, appCoroutineScope, dispatcherProvider) + return RealEmailProtectionInContextFeatureRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @Provides diff --git a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/feature/AutofillFeatureRepository.kt b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/feature/AutofillFeatureRepository.kt index 5579eb921e24..18c61845cfbd 100644 --- a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/feature/AutofillFeatureRepository.kt +++ b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/feature/AutofillFeatureRepository.kt @@ -35,13 +35,18 @@ class RealAutofillFeatureRepository( val database: AutofillDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : AutofillFeatureRepository { private val autofillDao: AutofillDao = database.autofillDao() override val exceptions = CopyOnWriteArrayList() init { - coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } } override fun updateAllExceptions(exceptions: List) { diff --git a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/feature/email/incontext/EmailProtectionInContextFeatureRepository.kt b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/feature/email/incontext/EmailProtectionInContextFeatureRepository.kt index 4bd04103cdd6..38c80aca19bf 100644 --- a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/feature/email/incontext/EmailProtectionInContextFeatureRepository.kt +++ b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/feature/email/incontext/EmailProtectionInContextFeatureRepository.kt @@ -30,13 +30,18 @@ class RealEmailProtectionInContextFeatureRepository( val database: EmailProtectionInContextDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : EmailProtectionInContextFeatureRepository { private val dao = database.emailInContextDao() override val exceptions = CopyOnWriteArrayList() init { - coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } } override fun updateAllExceptions(exceptions: List) { diff --git a/cookies/cookies-impl/src/main/java/com/duckduckgo/cookies/impl/di/CookiesModule.kt b/cookies/cookies-impl/src/main/java/com/duckduckgo/cookies/impl/di/CookiesModule.kt index fcb4258d0595..615879c4caf6 100644 --- a/cookies/cookies-impl/src/main/java/com/duckduckgo/cookies/impl/di/CookiesModule.kt +++ b/cookies/cookies-impl/src/main/java/com/duckduckgo/cookies/impl/di/CookiesModule.kt @@ -19,6 +19,7 @@ package com.duckduckgo.cookies.impl.di import android.content.Context import androidx.room.Room import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.cookies.store.ALL_MIGRATIONS import com.duckduckgo.cookies.store.CookiesDatabase @@ -56,8 +57,9 @@ object CookiesModule { database: CookiesDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): CookiesRepository { - return RealCookieRepository(database, appCoroutineScope, dispatcherProvider) + return RealCookieRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -78,7 +80,8 @@ object CookiesModule { database: CookiesDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): ContentScopeScriptsCookieRepository { - return RealContentScopeScriptsCookieRepository(database, appCoroutineScope, dispatcherProvider) + return RealContentScopeScriptsCookieRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } } diff --git a/cookies/cookies-store/src/main/java/com/duckduckgo/cookies/store/CookiesRepository.kt b/cookies/cookies-store/src/main/java/com/duckduckgo/cookies/store/CookiesRepository.kt index 149aff270b49..ef46f78d4ad7 100644 --- a/cookies/cookies-store/src/main/java/com/duckduckgo/cookies/store/CookiesRepository.kt +++ b/cookies/cookies-store/src/main/java/com/duckduckgo/cookies/store/CookiesRepository.kt @@ -32,6 +32,7 @@ class RealCookieRepository constructor( val database: CookiesDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : CookiesRepository { private val cookiesDao: CookiesDao = database.cookiesDao() @@ -41,7 +42,9 @@ class RealCookieRepository constructor( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/cookies/cookies-store/src/main/java/com/duckduckgo/cookies/store/contentscopescripts/ContentScopeScriptsCookieRepository.kt b/cookies/cookies-store/src/main/java/com/duckduckgo/cookies/store/contentscopescripts/ContentScopeScriptsCookieRepository.kt index 6064d95705f4..17026578f7b1 100644 --- a/cookies/cookies-store/src/main/java/com/duckduckgo/cookies/store/contentscopescripts/ContentScopeScriptsCookieRepository.kt +++ b/cookies/cookies-store/src/main/java/com/duckduckgo/cookies/store/contentscopescripts/ContentScopeScriptsCookieRepository.kt @@ -30,9 +30,10 @@ interface ContentScopeScriptsCookieRepository { } class RealContentScopeScriptsCookieRepository constructor( - private val database: CookiesDatabase, + database: CookiesDatabase, coroutineScope: CoroutineScope, - private val dispatcherProvider: DispatcherProvider, + dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : ContentScopeScriptsCookieRepository { private val contentScopeScriptsCookieDao: ContentScopeScriptsCookieDao = database.contentScopeScriptsCookieDao() @@ -40,7 +41,9 @@ class RealContentScopeScriptsCookieRepository constructor( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/cookies/cookies-store/src/test/java/com/duckduckgo/cookies/store/RealCookieRepositoryTest.kt b/cookies/cookies-store/src/test/java/com/duckduckgo/cookies/store/RealCookieRepositoryTest.kt index 6599eaf690c4..bf54252d0ef0 100644 --- a/cookies/cookies-store/src/test/java/com/duckduckgo/cookies/store/RealCookieRepositoryTest.kt +++ b/cookies/cookies-store/src/test/java/com/duckduckgo/cookies/store/RealCookieRepositoryTest.kt @@ -52,6 +52,7 @@ class RealCookieRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(cookieExceptionEntity.toFeatureException(), testee.exceptions.first()) @@ -67,6 +68,7 @@ class RealCookieRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(DEFAULT_THRESHOLD, testee.firstPartyCookiePolicy.threshold) @@ -81,6 +83,7 @@ class RealCookieRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) testee.updateAll(listOf(), policy) @@ -96,6 +99,7 @@ class RealCookieRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(1, testee.exceptions.size) assertEquals(THRESHOLD, testee.firstPartyCookiePolicy.threshold) diff --git a/di/src/main/java/com/duckduckgo/app/di/IsMainProcess.kt b/di/src/main/java/com/duckduckgo/app/di/IsMainProcess.kt new file mode 100644 index 000000000000..12b50fce5a67 --- /dev/null +++ b/di/src/main/java/com/duckduckgo/app/di/IsMainProcess.kt @@ -0,0 +1,23 @@ +/* + * 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.app.di + +import javax.inject.Qualifier + +@Retention(AnnotationRetention.BINARY) +@Qualifier +annotation class IsMainProcess diff --git a/element-hiding/element-hiding-impl/src/main/java/com/duckduckgo/elementhiding/impl/di/ElementHidingModule.kt b/element-hiding/element-hiding-impl/src/main/java/com/duckduckgo/elementhiding/impl/di/ElementHidingModule.kt index a05e03d26432..ad58c04198fc 100644 --- a/element-hiding/element-hiding-impl/src/main/java/com/duckduckgo/elementhiding/impl/di/ElementHidingModule.kt +++ b/element-hiding/element-hiding-impl/src/main/java/com/duckduckgo/elementhiding/impl/di/ElementHidingModule.kt @@ -19,6 +19,7 @@ package com.duckduckgo.elementhiding.impl.di import android.content.Context import androidx.room.Room import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.elementhiding.store.ALL_MIGRATIONS @@ -51,7 +52,8 @@ object ElementHidingModule { database: ElementHidingDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): ElementHidingRepository { - return RealElementHidingRepository(database, appCoroutineScope, dispatcherProvider) + return RealElementHidingRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } } diff --git a/element-hiding/element-hiding-store/src/main/java/com/duckduckgo/elementhiding/store/ElementHidingRepository.kt b/element-hiding/element-hiding-store/src/main/java/com/duckduckgo/elementhiding/store/ElementHidingRepository.kt index 09899df8908e..ca76191edd16 100644 --- a/element-hiding/element-hiding-store/src/main/java/com/duckduckgo/elementhiding/store/ElementHidingRepository.kt +++ b/element-hiding/element-hiding-store/src/main/java/com/duckduckgo/elementhiding/store/ElementHidingRepository.kt @@ -31,6 +31,7 @@ class RealElementHidingRepository constructor( val database: ElementHidingDatabase, val coroutineScope: CoroutineScope, val dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : ElementHidingRepository { private val elementHidingDao: ElementHidingDao = database.elementHidingDao() @@ -38,7 +39,9 @@ class RealElementHidingRepository constructor( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/element-hiding/element-hiding-store/src/test/java/com/duckduckgo/elementhiding/store/ElementHidingRepositoryTest.kt b/element-hiding/element-hiding-store/src/test/java/com/duckduckgo/elementhiding/store/ElementHidingRepositoryTest.kt index c9481a4c36be..23bf573fcf01 100644 --- a/element-hiding/element-hiding-store/src/test/java/com/duckduckgo/elementhiding/store/ElementHidingRepositoryTest.kt +++ b/element-hiding/element-hiding-store/src/test/java/com/duckduckgo/elementhiding/store/ElementHidingRepositoryTest.kt @@ -49,6 +49,7 @@ class ElementHidingRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) verify(mockElementHidingDao).get() @@ -64,6 +65,7 @@ class ElementHidingRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) verify(mockElementHidingDao).get() @@ -78,6 +80,7 @@ class ElementHidingRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) testee.updateAll(elementHidingEntity) diff --git a/fingerprint-protection/fingerprint-protection-impl/src/main/java/com/duckduckgo/fingerprintprotection/impl/di/FingerprintProtectionModule.kt b/fingerprint-protection/fingerprint-protection-impl/src/main/java/com/duckduckgo/fingerprintprotection/impl/di/FingerprintProtectionModule.kt index 5fa03cede049..db5b26082edd 100644 --- a/fingerprint-protection/fingerprint-protection-impl/src/main/java/com/duckduckgo/fingerprintprotection/impl/di/FingerprintProtectionModule.kt +++ b/fingerprint-protection/fingerprint-protection-impl/src/main/java/com/duckduckgo/fingerprintprotection/impl/di/FingerprintProtectionModule.kt @@ -19,6 +19,7 @@ package com.duckduckgo.fingerprintprotection.impl.di import android.content.Context import androidx.room.Room import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.fingerprintprotection.store.ALL_MIGRATIONS @@ -60,8 +61,9 @@ object FingerprintProtectionModule { database: FingerprintProtectionDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): FingerprintingBatteryRepository { - return RealFingerprintingBatteryRepository(database, appCoroutineScope, dispatcherProvider) + return RealFingerprintingBatteryRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -70,8 +72,9 @@ object FingerprintProtectionModule { database: FingerprintProtectionDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): FingerprintingCanvasRepository { - return RealFingerprintingCanvasRepository(database, appCoroutineScope, dispatcherProvider) + return RealFingerprintingCanvasRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -80,8 +83,9 @@ object FingerprintProtectionModule { database: FingerprintProtectionDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): FingerprintingHardwareRepository { - return RealFingerprintingHardwareRepository(database, appCoroutineScope, dispatcherProvider) + return RealFingerprintingHardwareRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -90,8 +94,9 @@ object FingerprintProtectionModule { database: FingerprintProtectionDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): FingerprintingScreenSizeRepository { - return RealFingerprintingScreenSizeRepository(database, appCoroutineScope, dispatcherProvider) + return RealFingerprintingScreenSizeRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -100,8 +105,9 @@ object FingerprintProtectionModule { database: FingerprintProtectionDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): FingerprintingTemporaryStorageRepository { - return RealFingerprintingTemporaryStorageRepository(database, appCoroutineScope, dispatcherProvider) + return RealFingerprintingTemporaryStorageRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) diff --git a/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingbattery/FingerprintingBatteryRepository.kt b/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingbattery/FingerprintingBatteryRepository.kt index 2b52c1e1394a..f1007ed33d06 100644 --- a/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingbattery/FingerprintingBatteryRepository.kt +++ b/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingbattery/FingerprintingBatteryRepository.kt @@ -33,6 +33,7 @@ class RealFingerprintingBatteryRepository constructor( val database: FingerprintProtectionDatabase, val coroutineScope: CoroutineScope, val dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : FingerprintingBatteryRepository { private val fingerprintingBatteryDao: FingerprintingBatteryDao = database.fingerprintingBatteryDao() @@ -40,7 +41,9 @@ class RealFingerprintingBatteryRepository constructor( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingcanvas/FingerprintingCanvasRepository.kt b/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingcanvas/FingerprintingCanvasRepository.kt index 911537fb830b..7d652aad7be0 100644 --- a/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingcanvas/FingerprintingCanvasRepository.kt +++ b/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingcanvas/FingerprintingCanvasRepository.kt @@ -29,10 +29,11 @@ interface FingerprintingCanvasRepository { var fingerprintingCanvasEntity: FingerprintingCanvasEntity } -class RealFingerprintingCanvasRepository constructor( +class RealFingerprintingCanvasRepository( val database: FingerprintProtectionDatabase, val coroutineScope: CoroutineScope, val dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : FingerprintingCanvasRepository { private val fingerprintingCanvasDao: FingerprintingCanvasDao = database.fingerprintingCanvasDao() @@ -40,7 +41,9 @@ class RealFingerprintingCanvasRepository constructor( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintinghardware/FingerprintingHardwareRepository.kt b/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintinghardware/FingerprintingHardwareRepository.kt index 284f13ec4b5b..dc3a87394d45 100644 --- a/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintinghardware/FingerprintingHardwareRepository.kt +++ b/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintinghardware/FingerprintingHardwareRepository.kt @@ -33,6 +33,7 @@ class RealFingerprintingHardwareRepository constructor( val database: FingerprintProtectionDatabase, val coroutineScope: CoroutineScope, val dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : FingerprintingHardwareRepository { private val fingerprintingHardwareDao: FingerprintingHardwareDao = database.fingerprintingHardwareDao() @@ -40,7 +41,9 @@ class RealFingerprintingHardwareRepository constructor( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingscreensize/FingerprintingScreenSizeRepository.kt b/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingscreensize/FingerprintingScreenSizeRepository.kt index e2075f6f41cf..f545dd440895 100644 --- a/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingscreensize/FingerprintingScreenSizeRepository.kt +++ b/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingscreensize/FingerprintingScreenSizeRepository.kt @@ -33,6 +33,7 @@ class RealFingerprintingScreenSizeRepository constructor( val database: FingerprintProtectionDatabase, val coroutineScope: CoroutineScope, val dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : FingerprintingScreenSizeRepository { private val fingerprintingScreenSizeDao: FingerprintingScreenSizeDao = database.fingerprintingScreenSizeDao() @@ -40,7 +41,9 @@ class RealFingerprintingScreenSizeRepository constructor( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingtemporarystorage/FingerprintingTemporaryStorageRepository.kt b/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingtemporarystorage/FingerprintingTemporaryStorageRepository.kt index 3e37c6e048f4..c695f4679b52 100644 --- a/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingtemporarystorage/FingerprintingTemporaryStorageRepository.kt +++ b/fingerprint-protection/fingerprint-protection-store/src/main/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingtemporarystorage/FingerprintingTemporaryStorageRepository.kt @@ -33,6 +33,7 @@ class RealFingerprintingTemporaryStorageRepository constructor( val database: FingerprintProtectionDatabase, val coroutineScope: CoroutineScope, val dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : FingerprintingTemporaryStorageRepository { private val fingerprintingTemporaryStorageDao: FingerprintingTemporaryStorageDao = database.fingerprintingTemporaryStorageDao() @@ -40,7 +41,9 @@ class RealFingerprintingTemporaryStorageRepository constructor( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingbattery/RealFingerprintingBatteryRepositoryTest.kt b/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingbattery/RealFingerprintingBatteryRepositoryTest.kt index 0ea717547fa6..90221bc6f6e8 100644 --- a/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingbattery/RealFingerprintingBatteryRepositoryTest.kt +++ b/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingbattery/RealFingerprintingBatteryRepositoryTest.kt @@ -51,6 +51,7 @@ class RealFingerprintingBatteryRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) verify(mockFingerprintingBatteryDao).get() @@ -66,6 +67,7 @@ class RealFingerprintingBatteryRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) verify(mockFingerprintingBatteryDao).get() @@ -80,6 +82,7 @@ class RealFingerprintingBatteryRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) testee.updateAll(fingerprintingBatteryEntity) diff --git a/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingcanvas/RealFingerprintingCanvasRepositoryTest.kt b/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingcanvas/RealFingerprintingCanvasRepositoryTest.kt index 8993ed0c7441..cd8cdb9e6e84 100644 --- a/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingcanvas/RealFingerprintingCanvasRepositoryTest.kt +++ b/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingcanvas/RealFingerprintingCanvasRepositoryTest.kt @@ -51,6 +51,7 @@ class RealFingerprintingCanvasRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) verify(mockFingerprintingCanvasDao).get() @@ -66,6 +67,7 @@ class RealFingerprintingCanvasRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) verify(mockFingerprintingCanvasDao).get() @@ -80,6 +82,7 @@ class RealFingerprintingCanvasRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) testee.updateAll(fingerprintingCanvasEntity) diff --git a/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintinghardware/RealFingerprintingHardwareRepositoryTest.kt b/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintinghardware/RealFingerprintingHardwareRepositoryTest.kt index 2c6452875e3c..5fdd7e6fbd7f 100644 --- a/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintinghardware/RealFingerprintingHardwareRepositoryTest.kt +++ b/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintinghardware/RealFingerprintingHardwareRepositoryTest.kt @@ -51,6 +51,7 @@ class RealFingerprintingHardwareRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) verify(mockFingerprintingHardwareDao).get() @@ -66,6 +67,7 @@ class RealFingerprintingHardwareRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) verify(mockFingerprintingHardwareDao).get() @@ -80,6 +82,7 @@ class RealFingerprintingHardwareRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) testee.updateAll(fingerprintingHardwareEntity) diff --git a/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingscreensize/RealFingerprintingScreenSizeRepositoryTest.kt b/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingscreensize/RealFingerprintingScreenSizeRepositoryTest.kt index 59bd77025399..648e5f9b1cd4 100644 --- a/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingscreensize/RealFingerprintingScreenSizeRepositoryTest.kt +++ b/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingscreensize/RealFingerprintingScreenSizeRepositoryTest.kt @@ -51,6 +51,7 @@ class RealFingerprintingScreenSizeRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) verify(mockFingerprintingScreenSizeDao).get() @@ -66,6 +67,7 @@ class RealFingerprintingScreenSizeRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) verify(mockFingerprintingScreenSizeDao).get() @@ -80,6 +82,7 @@ class RealFingerprintingScreenSizeRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) testee.updateAll(fingerprintingScreenSizeEntity) diff --git a/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingtemporarystorage/RealFingerprintingTemporaryStorageRepositoryTest.kt b/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingtemporarystorage/RealFingerprintingTemporaryStorageRepositoryTest.kt index a41be32943a7..5550ad352a72 100644 --- a/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingtemporarystorage/RealFingerprintingTemporaryStorageRepositoryTest.kt +++ b/fingerprint-protection/fingerprint-protection-store/src/test/java/com/duckduckgo/fingerprintprotection/store/features/fingerprintingtemporarystorage/RealFingerprintingTemporaryStorageRepositoryTest.kt @@ -51,6 +51,7 @@ class RealFingerprintingTemporaryStorageRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) verify(mockFingerprintingTemporaryStorageDao).get() @@ -66,6 +67,7 @@ class RealFingerprintingTemporaryStorageRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) verify(mockFingerprintingTemporaryStorageDao).get() @@ -80,6 +82,7 @@ class RealFingerprintingTemporaryStorageRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) testee.updateAll(fingerprintingTemporaryStorageEntity) diff --git a/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/di/PrivacyConfigModule.kt b/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/di/PrivacyConfigModule.kt index 4fb7118a53cc..50f7a4fba056 100644 --- a/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/di/PrivacyConfigModule.kt +++ b/privacy-config/privacy-config-impl/src/main/java/com/duckduckgo/privacy/config/impl/di/PrivacyConfigModule.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.SharedPreferences import androidx.room.Room import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.privacy.config.store.ALL_MIGRATIONS @@ -88,8 +89,9 @@ object DatabaseModule { database: PrivacyConfigDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): TrackerAllowlistRepository { - return RealTrackerAllowlistRepository(database, appCoroutineScope, dispatcherProvider) + return RealTrackerAllowlistRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -98,8 +100,9 @@ object DatabaseModule { database: PrivacyConfigDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): ContentBlockingRepository { - return RealContentBlockingRepository(database, appCoroutineScope, dispatcherProvider) + return RealContentBlockingRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -121,8 +124,9 @@ object DatabaseModule { database: PrivacyConfigDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): GpcRepository { - return RealGpcRepository(gpcDataStore, database, appCoroutineScope, dispatcherProvider) + return RealGpcRepository(gpcDataStore, database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -131,8 +135,9 @@ object DatabaseModule { database: PrivacyConfigDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): HttpsRepository { - return RealHttpsRepository(database, appCoroutineScope, dispatcherProvider) + return RealHttpsRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -141,8 +146,9 @@ object DatabaseModule { database: PrivacyConfigDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): UnprotectedTemporaryRepository { - return RealUnprotectedTemporaryRepository(database, appCoroutineScope, dispatcherProvider) + return RealUnprotectedTemporaryRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -157,8 +163,9 @@ object DatabaseModule { database: PrivacyConfigDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): DrmRepository { - return RealDrmRepository(database, appCoroutineScope, dispatcherProvider) + return RealDrmRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -167,8 +174,9 @@ object DatabaseModule { database: PrivacyConfigDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): AmpLinksRepository { - return RealAmpLinksRepository(database, appCoroutineScope, dispatcherProvider) + return RealAmpLinksRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -177,8 +185,9 @@ object DatabaseModule { database: PrivacyConfigDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): TrackingParametersRepository { - return RealTrackingParametersRepository(database, appCoroutineScope, dispatcherProvider) + return RealTrackingParametersRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) @@ -187,7 +196,8 @@ object DatabaseModule { database: PrivacyConfigDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): UserAgentRepository { - return RealUserAgentRepository(database, appCoroutineScope, dispatcherProvider) + return RealUserAgentRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } } diff --git a/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/RealPrivacyConfigPersisterTest.kt b/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/RealPrivacyConfigPersisterTest.kt index fd842a40b106..8416bbaf64a0 100644 --- a/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/RealPrivacyConfigPersisterTest.kt +++ b/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/RealPrivacyConfigPersisterTest.kt @@ -105,6 +105,7 @@ class RealPrivacyConfigPersisterTest { db, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) } diff --git a/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/ReferenceTestUtilities.kt b/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/ReferenceTestUtilities.kt index 2a9caf4a81e9..8523f337341e 100644 --- a/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/ReferenceTestUtilities.kt +++ b/privacy-config/privacy-config-impl/src/test/java/com/duckduckgo/privacy/config/impl/ReferenceTestUtilities.kt @@ -57,12 +57,12 @@ class ReferenceTestUtilities( var privacyRepository: PrivacyConfigRepository = RealPrivacyConfigRepository(db) var privacyFeatureTogglesRepository: PrivacyFeatureTogglesRepository = mock() - var unprotectedTemporaryRepository: UnprotectedTemporaryRepository = RealUnprotectedTemporaryRepository(db, TestScope(), dispatcherProvider) - var contentBlockingRepository: ContentBlockingRepository = RealContentBlockingRepository(db, TestScope(), dispatcherProvider) - var httpsRepository: HttpsRepository = RealHttpsRepository(db, TestScope(), dispatcherProvider) - var drmRepository: DrmRepository = RealDrmRepository(db, TestScope(), dispatcherProvider) - var gpcRepository: GpcRepository = RealGpcRepository(mock(), db, TestScope(), dispatcherProvider) - var trackerAllowlistRepository: TrackerAllowlistRepository = RealTrackerAllowlistRepository(db, TestScope(), dispatcherProvider) + var unprotectedTemporaryRepository: UnprotectedTemporaryRepository = RealUnprotectedTemporaryRepository(db, TestScope(), dispatcherProvider, true) + var contentBlockingRepository: ContentBlockingRepository = RealContentBlockingRepository(db, TestScope(), dispatcherProvider, true) + var httpsRepository: HttpsRepository = RealHttpsRepository(db, TestScope(), dispatcherProvider, true) + var drmRepository: DrmRepository = RealDrmRepository(db, TestScope(), dispatcherProvider, true) + var gpcRepository: GpcRepository = RealGpcRepository(mock(), db, TestScope(), dispatcherProvider, true) + var trackerAllowlistRepository: TrackerAllowlistRepository = RealTrackerAllowlistRepository(db, TestScope(), dispatcherProvider, true) // Add your plugin to this list in order for it to be tested against some basic reference tests private fun getPrivacyFeaturePlugins(): List { diff --git a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/amplinks/AmpLinksRepository.kt b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/amplinks/AmpLinksRepository.kt index 7b94afcd8670..5faea6a5abd7 100644 --- a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/amplinks/AmpLinksRepository.kt +++ b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/amplinks/AmpLinksRepository.kt @@ -34,6 +34,7 @@ class RealAmpLinksRepository( val database: PrivacyConfigDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : AmpLinksRepository { private val ampLinksDao: AmpLinksDao = database.ampLinksDao() @@ -44,7 +45,9 @@ class RealAmpLinksRepository( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/contentblocking/ContentBlockingRepository.kt b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/contentblocking/ContentBlockingRepository.kt index 2bec947b0dae..a19e48f1e826 100644 --- a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/contentblocking/ContentBlockingRepository.kt +++ b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/contentblocking/ContentBlockingRepository.kt @@ -34,13 +34,18 @@ class RealContentBlockingRepository( val database: PrivacyConfigDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : ContentBlockingRepository { private val contentBlockingDao: ContentBlockingDao = database.contentBlockingDao() override val exceptions = CopyOnWriteArrayList() init { - coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } } override fun updateAll(exceptions: List) { diff --git a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/drm/DrmRepository.kt b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/drm/DrmRepository.kt index 7b5fb4a16908..6946cb02020c 100644 --- a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/drm/DrmRepository.kt +++ b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/drm/DrmRepository.kt @@ -34,6 +34,7 @@ class RealDrmRepository( val database: PrivacyConfigDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : DrmRepository { @@ -42,7 +43,9 @@ class RealDrmRepository( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/gpc/GpcRepository.kt b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/gpc/GpcRepository.kt index 75311997d0a7..a7d71b7404a0 100644 --- a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/gpc/GpcRepository.kt +++ b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/gpc/GpcRepository.kt @@ -49,6 +49,7 @@ class RealGpcRepository( val database: PrivacyConfigDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : GpcRepository { private val gpcExceptionsDao: GpcExceptionsDao = database.gpcExceptionsDao() @@ -59,7 +60,11 @@ class RealGpcRepository( override var gpcContentScopeConfig: String = emptyJson init { - coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } } override fun updateAll( diff --git a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/https/HttpsRepository.kt b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/https/HttpsRepository.kt index 748150ff1aee..0daaa9f7e16b 100644 --- a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/https/HttpsRepository.kt +++ b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/https/HttpsRepository.kt @@ -34,13 +34,18 @@ class RealHttpsRepository( val database: PrivacyConfigDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : HttpsRepository { private val httpsDao: HttpsDao = database.httpsDao() override val exceptions = CopyOnWriteArrayList() init { - coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } } override fun updateAll(exceptions: List) { diff --git a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/trackerallowlist/TrackerAllowlistRepository.kt b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/trackerallowlist/TrackerAllowlistRepository.kt index 6719aa55b5ee..65a45c634baf 100644 --- a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/trackerallowlist/TrackerAllowlistRepository.kt +++ b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/trackerallowlist/TrackerAllowlistRepository.kt @@ -32,13 +32,18 @@ class RealTrackerAllowlistRepository( database: PrivacyConfigDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : TrackerAllowlistRepository { private val trackerAllowlistDao: TrackerAllowlistDao = database.trackerAllowlistDao() override val exceptions = CopyOnWriteArrayList() init { - coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } } override fun updateAll(exceptions: List) { diff --git a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/trackingparameters/TrackingParametersRepository.kt b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/trackingparameters/TrackingParametersRepository.kt index f2163846dc97..852757645fc2 100644 --- a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/trackingparameters/TrackingParametersRepository.kt +++ b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/trackingparameters/TrackingParametersRepository.kt @@ -33,6 +33,7 @@ class RealTrackingParametersRepository( val database: PrivacyConfigDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : TrackingParametersRepository { private val trackingParametersDao: TrackingParametersDao = database.trackingParametersDao() @@ -42,7 +43,9 @@ class RealTrackingParametersRepository( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/unprotectedtemporary/UnprotectedTemporaryRepository.kt b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/unprotectedtemporary/UnprotectedTemporaryRepository.kt index d954a42cf0e8..af3aa1c24f6b 100644 --- a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/unprotectedtemporary/UnprotectedTemporaryRepository.kt +++ b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/unprotectedtemporary/UnprotectedTemporaryRepository.kt @@ -34,6 +34,7 @@ class RealUnprotectedTemporaryRepository( val database: PrivacyConfigDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : UnprotectedTemporaryRepository { private val unprotectedTemporaryDao: UnprotectedTemporaryDao = @@ -41,7 +42,11 @@ class RealUnprotectedTemporaryRepository( override val exceptions = CopyOnWriteArrayList() init { - coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } } override fun updateAll(exceptions: List) { diff --git a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/useragent/UserAgentRepository.kt b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/useragent/UserAgentRepository.kt index 3eb1e7d3e274..b186c40f684d 100644 --- a/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/useragent/UserAgentRepository.kt +++ b/privacy-config/privacy-config-store/src/main/java/com/duckduckgo/privacy/config/store/features/useragent/UserAgentRepository.kt @@ -51,6 +51,7 @@ class RealUserAgentRepository( val database: PrivacyConfigDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : UserAgentRepository { private val userAgentDao: UserAgentDao = database.userAgentDao() @@ -69,7 +70,11 @@ class RealUserAgentRepository( override var ddgFixedUserAgentVersions = CopyOnWriteArrayList() init { - coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } } override fun updateAll( diff --git a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/amplinks/RealAmpLinksRepositoryTest.kt b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/amplinks/RealAmpLinksRepositoryTest.kt index 148e36a3173a..d5c50667b675 100644 --- a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/amplinks/RealAmpLinksRepositoryTest.kt +++ b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/amplinks/RealAmpLinksRepositoryTest.kt @@ -47,6 +47,7 @@ class RealAmpLinksRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) } @@ -58,6 +59,7 @@ class RealAmpLinksRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(ampLinkExceptionEntity.toFeatureException(), testee.exceptions.first()) @@ -71,6 +73,7 @@ class RealAmpLinksRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) testee.updateAll(listOf(), listOf(), listOf()) @@ -86,6 +89,7 @@ class RealAmpLinksRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(1, testee.exceptions.size) assertEquals(1, testee.ampLinkFormats.size) diff --git a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/contentblocking/RealContentBlockingRepositoryTest.kt b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/contentblocking/RealContentBlockingRepositoryTest.kt index 32c33c430988..43770d431a38 100644 --- a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/contentblocking/RealContentBlockingRepositoryTest.kt +++ b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/contentblocking/RealContentBlockingRepositoryTest.kt @@ -55,6 +55,7 @@ class RealContentBlockingRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals( @@ -71,6 +72,7 @@ class RealContentBlockingRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) testee.updateAll(listOf()) @@ -87,6 +89,7 @@ class RealContentBlockingRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(1, testee.exceptions.size) reset(mockContentBlockingDao) diff --git a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/drm/RealDrmRepositoryTest.kt b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/drm/RealDrmRepositoryTest.kt index 52e30f20da87..600b6e77f194 100644 --- a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/drm/RealDrmRepositoryTest.kt +++ b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/drm/RealDrmRepositoryTest.kt @@ -50,14 +50,14 @@ class RealDrmRepositoryTest { fun whenRepositoryIsCreatedThenExceptionsLoadedIntoMemory() = runTest { givenDrmDaoContainsExceptions() - testee = RealDrmRepository(mockDatabase, this, coroutineRule.testDispatcherProvider) + testee = RealDrmRepository(mockDatabase, this, coroutineRule.testDispatcherProvider, true) assertEquals(drmException.toFeatureException(), testee.exceptions.first()) } @Test fun whenUpdateAllThenUpdateAllCalled() = runTest { - testee = RealDrmRepository(mockDatabase, this, coroutineRule.testDispatcherProvider) + testee = RealDrmRepository(mockDatabase, this, coroutineRule.testDispatcherProvider, true) testee.updateAll(listOf()) @@ -67,7 +67,7 @@ class RealDrmRepositoryTest { @Test fun whenUpdateAllThenPreviousExceptionsAreCleared() = runTest { givenDrmDaoContainsExceptions() - testee = RealDrmRepository(mockDatabase, this, coroutineRule.testDispatcherProvider) + testee = RealDrmRepository(mockDatabase, this, coroutineRule.testDispatcherProvider, true) assertEquals(1, testee.exceptions.size) reset(mockDrmDao) diff --git a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/gpc/RealGpcRepositoryTest.kt b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/gpc/RealGpcRepositoryTest.kt index 9a9b99c40b11..fd2744ed46b9 100644 --- a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/gpc/RealGpcRepositoryTest.kt +++ b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/gpc/RealGpcRepositoryTest.kt @@ -56,6 +56,7 @@ class RealGpcRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) } @@ -69,6 +70,7 @@ class RealGpcRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(gpcException.toGpcException(), testee.exceptions.first()) @@ -83,6 +85,7 @@ class RealGpcRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) testee.updateAll(listOf(), listOf(), configEntity) @@ -102,6 +105,7 @@ class RealGpcRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(1, testee.exceptions.size) reset(mockGpcExceptionsDao) @@ -121,6 +125,7 @@ class RealGpcRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(1, testee.headerEnabledSites.size) reset(mockGpcHeadersDao) @@ -140,6 +145,7 @@ class RealGpcRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(configEntity.config, testee.gpcContentScopeConfig) givenGpcDaoContainsConfig(configEntity2) diff --git a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/https/RealHttpsRepositoryTest.kt b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/https/RealHttpsRepositoryTest.kt index e72bedcfd823..cc2b22c2a4b3 100644 --- a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/https/RealHttpsRepositoryTest.kt +++ b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/https/RealHttpsRepositoryTest.kt @@ -49,6 +49,7 @@ class RealHttpsRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) } @@ -61,6 +62,7 @@ class RealHttpsRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(httpException.toFeatureException(), testee.exceptions.first()) @@ -74,6 +76,7 @@ class RealHttpsRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) testee.updateAll(listOf()) @@ -90,6 +93,7 @@ class RealHttpsRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(1, testee.exceptions.size) reset(mockHttpsDao) diff --git a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/trackerallowlist/RealTrackerAllowlistRepositoryTest.kt b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/trackerallowlist/RealTrackerAllowlistRepositoryTest.kt index 7664d6623dfe..35f5e8c2e963 100644 --- a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/trackerallowlist/RealTrackerAllowlistRepositoryTest.kt +++ b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/trackerallowlist/RealTrackerAllowlistRepositoryTest.kt @@ -49,6 +49,7 @@ class RealTrackerAllowlistRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) } @@ -61,6 +62,7 @@ class RealTrackerAllowlistRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(trackerAllowlistEntity, testee.exceptions.first()) @@ -74,6 +76,7 @@ class RealTrackerAllowlistRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) testee.updateAll(listOf()) @@ -90,6 +93,7 @@ class RealTrackerAllowlistRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(1, testee.exceptions.size) reset(mockTrackerAllowlistDao) diff --git a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/trackingparameters/RealTrackingParametersRepositoryTest.kt b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/trackingparameters/RealTrackingParametersRepositoryTest.kt index 411020da5181..e98a894bc55d 100644 --- a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/trackingparameters/RealTrackingParametersRepositoryTest.kt +++ b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/trackingparameters/RealTrackingParametersRepositoryTest.kt @@ -47,6 +47,7 @@ class RealTrackingParametersRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) } @@ -58,6 +59,7 @@ class RealTrackingParametersRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(trackingParameterExceptionEntity.toFeatureException(), testee.exceptions.first()) @@ -70,6 +72,7 @@ class RealTrackingParametersRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) testee.updateAll(listOf(), listOf()) @@ -85,6 +88,7 @@ class RealTrackingParametersRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(1, testee.exceptions.size) assertEquals(1, testee.parameters.size) diff --git a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/unprotectedtemporary/RealUnprotectedTemporaryRepositoryTest.kt b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/unprotectedtemporary/RealUnprotectedTemporaryRepositoryTest.kt index cf1f0df94781..c6e0cc701cef 100644 --- a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/unprotectedtemporary/RealUnprotectedTemporaryRepositoryTest.kt +++ b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/unprotectedtemporary/RealUnprotectedTemporaryRepositoryTest.kt @@ -49,6 +49,7 @@ class RealUnprotectedTemporaryRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) } @@ -61,6 +62,7 @@ class RealUnprotectedTemporaryRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(unprotectedTemporaryException.toFeatureException(), testee.exceptions.first()) @@ -74,6 +76,7 @@ class RealUnprotectedTemporaryRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) testee.updateAll(listOf()) @@ -90,6 +93,7 @@ class RealUnprotectedTemporaryRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(1, testee.exceptions.size) reset(mockUnprotectedTemporaryDao) diff --git a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/useragent/RealUserAgentRepositoryTest.kt b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/useragent/RealUserAgentRepositoryTest.kt index 34d06dc24920..bf413a2bdceb 100644 --- a/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/useragent/RealUserAgentRepositoryTest.kt +++ b/privacy-config/privacy-config-store/src/test/java/com/duckduckgo/privacy/config/store/features/useragent/RealUserAgentRepositoryTest.kt @@ -59,6 +59,7 @@ class RealUserAgentRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) } @@ -71,6 +72,7 @@ class RealUserAgentRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(testee.omitApplicationExceptions.first(), actual) @@ -86,6 +88,7 @@ class RealUserAgentRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(testee.closestUserAgentState, true) @@ -104,6 +107,7 @@ class RealUserAgentRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) testee.updateAll(listOf(), listOf(), anyOrNull(), listOf()) @@ -120,6 +124,7 @@ class RealUserAgentRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(1, testee.defaultExceptions.size) assertEquals(1, testee.omitApplicationExceptions.size) @@ -142,6 +147,7 @@ class RealUserAgentRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) assertEquals(true, testee.closestUserAgentState) assertEquals(true, testee.ddgFixedUserAgentState) diff --git a/request-filterer/request-filterer-impl/src/main/java/com/duckduckgo/request/filterer/impl/di/RequestFiltererModule.kt b/request-filterer/request-filterer-impl/src/main/java/com/duckduckgo/request/filterer/impl/di/RequestFiltererModule.kt index 271c4fb3d023..4b245108e91f 100644 --- a/request-filterer/request-filterer-impl/src/main/java/com/duckduckgo/request/filterer/impl/di/RequestFiltererModule.kt +++ b/request-filterer/request-filterer-impl/src/main/java/com/duckduckgo/request/filterer/impl/di/RequestFiltererModule.kt @@ -19,6 +19,7 @@ package com.duckduckgo.request.filterer.impl.di import android.content.Context import androidx.room.Room import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.request.filterer.store.ALL_MIGRATIONS @@ -54,8 +55,9 @@ object RequestFiltererModule { database: RequestFiltererDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): RequestFiltererRepository { - return RealRequestFiltererRepository(database, appCoroutineScope, dispatcherProvider) + return RealRequestFiltererRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } @SingleInstanceIn(AppScope::class) diff --git a/request-filterer/request-filterer-store/src/main/java/com/duckduckgo/request/filterer/store/RequestFiltererRepository.kt b/request-filterer/request-filterer-store/src/main/java/com/duckduckgo/request/filterer/store/RequestFiltererRepository.kt index 589e665e7cf6..40494b49ee13 100644 --- a/request-filterer/request-filterer-store/src/main/java/com/duckduckgo/request/filterer/store/RequestFiltererRepository.kt +++ b/request-filterer/request-filterer-store/src/main/java/com/duckduckgo/request/filterer/store/RequestFiltererRepository.kt @@ -32,6 +32,7 @@ class RealRequestFiltererRepository( val database: RequestFiltererDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : RequestFiltererRepository { private val requestFiltererDao: RequestFiltererDao = database.requestFiltererDao() @@ -41,7 +42,9 @@ class RealRequestFiltererRepository( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/request-filterer/request-filterer-store/src/test/java/com/duckduckgo/request/filterer/store/RealRequestFiltererRepositoryTest.kt b/request-filterer/request-filterer-store/src/test/java/com/duckduckgo/request/filterer/store/RealRequestFiltererRepositoryTest.kt index 368757746cb0..b61cd9d98fe3 100644 --- a/request-filterer/request-filterer-store/src/test/java/com/duckduckgo/request/filterer/store/RealRequestFiltererRepositoryTest.kt +++ b/request-filterer/request-filterer-store/src/test/java/com/duckduckgo/request/filterer/store/RealRequestFiltererRepositoryTest.kt @@ -51,6 +51,7 @@ class RealRequestFiltererRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(requestFiltererExceptionEntity.toFeatureException(), testee.exceptions.first()) @@ -65,6 +66,7 @@ class RealRequestFiltererRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(DEFAULT_WINDOW_IN_MS, testee.settings.windowInMs) @@ -78,6 +80,7 @@ class RealRequestFiltererRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) testee.updateAll(listOf(), policy) @@ -93,6 +96,7 @@ class RealRequestFiltererRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + true, ) assertEquals(1, testee.exceptions.size) assertEquals(WINDOW_IN_MS, testee.settings.windowInMs) diff --git a/runtime-checks/runtime-checks-impl/src/main/java/com/duckduckgo/runtimechecks/impl/di/RuntimeChecksModule.kt b/runtime-checks/runtime-checks-impl/src/main/java/com/duckduckgo/runtimechecks/impl/di/RuntimeChecksModule.kt index 6d985c73e686..37356bab98a3 100644 --- a/runtime-checks/runtime-checks-impl/src/main/java/com/duckduckgo/runtimechecks/impl/di/RuntimeChecksModule.kt +++ b/runtime-checks/runtime-checks-impl/src/main/java/com/duckduckgo/runtimechecks/impl/di/RuntimeChecksModule.kt @@ -19,6 +19,7 @@ package com.duckduckgo.runtimechecks.impl.di import android.content.Context import androidx.room.Room import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.runtimechecks.store.ALL_MIGRATIONS @@ -51,7 +52,8 @@ object RuntimeChecksModule { database: RuntimeChecksDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): RuntimeChecksRepository { - return RealRuntimeChecksRepository(database, appCoroutineScope, dispatcherProvider) + return RealRuntimeChecksRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } } diff --git a/runtime-checks/runtime-checks-store/src/main/java/com/duckduckgo/runtimechecks/store/RuntimeChecksRepository.kt b/runtime-checks/runtime-checks-store/src/main/java/com/duckduckgo/runtimechecks/store/RuntimeChecksRepository.kt index bb19bcf58c65..b1071800c192 100644 --- a/runtime-checks/runtime-checks-store/src/main/java/com/duckduckgo/runtimechecks/store/RuntimeChecksRepository.kt +++ b/runtime-checks/runtime-checks-store/src/main/java/com/duckduckgo/runtimechecks/store/RuntimeChecksRepository.kt @@ -28,9 +28,10 @@ interface RuntimeChecksRepository { } class RealRuntimeChecksRepository constructor( - private val database: RuntimeChecksDatabase, + database: RuntimeChecksDatabase, coroutineScope: CoroutineScope, - private val dispatcherProvider: DispatcherProvider, + dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : RuntimeChecksRepository { private val runtimeChecksDao: RuntimeChecksDao = database.runtimeChecksDao() @@ -38,7 +39,9 @@ class RealRuntimeChecksRepository constructor( init { coroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/runtime-checks/runtime-checks-store/src/test/java/com/duckduckgo/runtimechecks/store/RuntimeChecksRepositoryTest.kt b/runtime-checks/runtime-checks-store/src/test/java/com/duckduckgo/runtimechecks/store/RuntimeChecksRepositoryTest.kt index 72ddc8b43f99..6aba7197ca28 100644 --- a/runtime-checks/runtime-checks-store/src/test/java/com/duckduckgo/runtimechecks/store/RuntimeChecksRepositoryTest.kt +++ b/runtime-checks/runtime-checks-store/src/test/java/com/duckduckgo/runtimechecks/store/RuntimeChecksRepositoryTest.kt @@ -49,6 +49,7 @@ class RuntimeChecksRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) verify(mockRuntimeChecksDao).get() @@ -64,6 +65,7 @@ class RuntimeChecksRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) verify(mockRuntimeChecksDao).get() @@ -78,6 +80,7 @@ class RuntimeChecksRepositoryTest { mockDatabase, TestScope(), coroutineRule.testDispatcherProvider, + isMainProcess = true, ) testee.updateAll(runtimeChecksEntity) diff --git a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/drmblock/DrmBlockRepository.kt b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/drmblock/DrmBlockRepository.kt index 659f80fc11d6..c2853731e540 100644 --- a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/drmblock/DrmBlockRepository.kt +++ b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/drmblock/DrmBlockRepository.kt @@ -17,6 +17,7 @@ package com.duckduckgo.site.permissions.impl.drmblock import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.feature.toggles.api.FeatureExceptions.FeatureException @@ -41,13 +42,16 @@ class RealDrmBlockRepository @Inject constructor( val drmBlockDao: DrmBlockDao, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ) : DrmBlockRepository { override val exceptions = CopyOnWriteArrayList() init { appCoroutineScope.launch(dispatcherProvider.io()) { - loadToMemory() + if (isMainProcess) { + loadToMemory() + } } } diff --git a/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/di/VoiceSearchModule.kt b/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/di/VoiceSearchModule.kt index a42844d39a8f..5f16ad170f1c 100644 --- a/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/di/VoiceSearchModule.kt +++ b/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/di/VoiceSearchModule.kt @@ -19,6 +19,7 @@ package com.duckduckgo.voice.impl.di import android.content.Context import androidx.room.Room import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.voice.api.VoiceSearchStatusListener @@ -66,7 +67,8 @@ object VoiceSearchModule { database: VoiceSearchDatabase, @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, ): VoiceSearchFeatureRepository { - return RealVoiceSearchFeatureRepository(database, appCoroutineScope, dispatcherProvider) + return RealVoiceSearchFeatureRepository(database, appCoroutineScope, dispatcherProvider, isMainProcess) } } diff --git a/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/remoteconfig/VoiceSearchFeatureRepository.kt b/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/remoteconfig/VoiceSearchFeatureRepository.kt index caf74a5963e0..5294b8c66a97 100644 --- a/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/remoteconfig/VoiceSearchFeatureRepository.kt +++ b/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/remoteconfig/VoiceSearchFeatureRepository.kt @@ -35,6 +35,7 @@ class RealVoiceSearchFeatureRepository constructor( database: VoiceSearchDatabase, coroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, ) : VoiceSearchFeatureRepository { private val voiceSearchDao: VoiceSearchDao = database.voiceSearchDao() @@ -44,7 +45,11 @@ class RealVoiceSearchFeatureRepository constructor( get() = _minVersion init { - coroutineScope.launch(dispatcherProvider.io()) { loadToMemory() } + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } } override fun updateAllExceptions(exceptions: List, minVersion: Int) { From 43ad633b9e3c67bb0e8e6a8a7713c4e2d7b2ac64 Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Mon, 19 Feb 2024 22:23:17 +0000 Subject: [PATCH 11/19] Surface the last-used password first in Autofill dialog for filling passwords (#4181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/488551667048375/1206534334888412/f ### Description Adds `last-used` as a field for saved credentials so that it can used in the sort order when presenting a list of passwords to autofill into a form. ### Steps to test this PR #### DB upgrade - [ ] Install from `develop` and save a login - [ ] Install from this branch, visit Passwords and ensure the login is still savedd ℹ️ When instructions say to add a saved login, it means: - visiting `Overflow -> Passwords` - Adding a login using the ➕ button - Ensure it both a username and a password #### Testing new sort order (perfect matches) - [ ] Add a saved login for `fill.dev` with username `A` - [ ] Add a saved login for `fill.dev` with username `B` - [ ] Add a saved login for `fill.dev` with username `C` - [ ] Visit `https://fill.dev/form/login-simple` - [ ] Verify that `C` will be the primary CTA (blue button) [ at this point, there are no last-used timestamps and so the last-modified will be used instead ] - [ ] Choose to use `B` (ℹ️ you can hit back instead of authenticating; by design this still counts as a selection and makes testing faster) - [ ] Reload the login form - [ ] Verify that `B` is now the primary CTA, then `C` is in the middle and `A` is at the bottom [ `B` is primary because it had a last-used timestamp and the others are sorted below it using their `last-modified` times ] - [ ] This time, choose `A` - [ ] Return to the login form (refresh is required) - [ ] Verify that `A` is now the primary CTA, then `B` is below and `C` is at the bottom [ `A` has the most recent last-used, `B` has the next most recent last-used and `C` is sorted last since it doesn't have any last-used value] #### Testing sort order (imperfect matches) - [ ] Add a saved login for `foo.fill.dev` with username `D` - [ ] Add a saved login for `foo.fill.dev` with username `E` - [ ] Reload the login form - [ ] Verify that primary button is still `A` in the top "From This Website" section [ these are the URL-perfect matches ] - [ ] Verify that `E` appears above `D` [ no last-used, so relying on last modified timestamps ] - [ ] Choose to use `D` - [ ] Return to the form (+ reload) - [ ] Verify that primary CTA is still `A` [ exact URL matches rank higher than last-used from an imperfect match ] - [ ] Verify that the order in the bottom group is now `D` above `E` --- .../api/domain/app/LoginCredentials.kt | 3 +- .../impl/SecureStoreBackedAutofillStore.kt | 9 +- .../impl/securestorage/SecureStorage.kt | 2 + .../impl/securestorage/SecureStorageModels.kt | 2 + .../impl/store/InternalAutofillStore.kt | 4 +- .../AutofillSelectCredentialsGrouper.kt | 38 ++++++--- .../AutofillSelectCredentialsSorter.kt | 85 +++++++++++++++++++ .../ResultHandlerCredentialSelection.kt | 11 +++ .../impl/RealDuckAddressLoginCreatorTest.kt | 4 +- .../SecureStoreBackedAutofillStoreTest.kt | 21 ++++- .../securestorage/RealSecureStorageTest.kt | 1 + .../ResultHandlerUseGeneratedPasswordTest.kt | 6 +- .../LastUpdatedCredentialSorterTest.kt | 81 ++++++++++++++++++ .../selecting/LastUsedCredentialSorterTest.kt | 81 ++++++++++++++++++ ...ealAutofillSelectCredentialsGrouperTest.kt | 66 +++++++++++++- .../ResultHandlerCredentialSelectionTest.kt | 3 + .../store/db/SecureStorageDatabase.kt | 7 +- .../store/db/WebsiteLoginCredentialsEntity.kt | 1 + .../RealSecureStorageRepositoryTest.kt | 2 + 19 files changed, 400 insertions(+), 27 deletions(-) create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsSorter.kt create mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/LastUpdatedCredentialSorterTest.kt create mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/LastUsedCredentialSorterTest.kt diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/domain/app/LoginCredentials.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/domain/app/LoginCredentials.kt index 15574034b768..820d0933b4d8 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/domain/app/LoginCredentials.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/domain/app/LoginCredentials.kt @@ -32,12 +32,13 @@ data class LoginCredentials( val domainTitle: String? = null, val notes: String? = null, val lastUpdatedMillis: Long? = null, + val lastUsedMillis: Long? = null, ) : Parcelable, Serializable { override fun toString(): String { return """ LoginCredentials( id=$id, domain=$domain, username=$username, password=********, domainTitle=$domainTitle, - lastUpdatedMillis=$lastUpdatedMillis, notesLength=${notes?.length ?: 0} + lastUpdatedMillis=$lastUpdatedMillis, lastUsedMillis=$lastUsedMillis, notesLength=${notes?.length ?: 0} ) """.trimIndent() } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStore.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStore.kt index 2844d59f5e6d..24abececc559 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStore.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStore.kt @@ -202,14 +202,15 @@ class SecureStoreBackedAutofillStore @Inject constructor( return savedCredentials.map { it.toLoginCredentials() } } - override suspend fun updateCredentials(credentials: LoginCredentials): LoginCredentials? { + override suspend fun updateCredentials(credentials: LoginCredentials, refreshLastUpdatedTimestamp: Boolean): LoginCredentials? { val cleanedDomain: String? = credentials.domain?.let { autofillUrlMatcher.cleanRawUrl(it) } + val lastUpdated = if (refreshLastUpdatedTimestamp) lastUpdatedTimeProvider.getInMillis() else credentials.lastUpdatedMillis + return secureStorage.updateWebsiteLoginDetailsWithCredentials( - credentials.copy(lastUpdatedMillis = lastUpdatedTimeProvider.getInMillis(), domain = cleanedDomain) - .toWebsiteLoginCredentials(), + credentials.copy(lastUpdatedMillis = lastUpdated, domain = cleanedDomain).toWebsiteLoginCredentials(), )?.toLoginCredentials()?.also { syncCredentialsListener.onCredentialUpdated(it.id!!) } @@ -317,6 +318,7 @@ class SecureStoreBackedAutofillStore @Inject constructor( domainTitle = details.domainTitle, notes = notes, lastUpdatedMillis = details.lastUpdatedMillis, + lastUsedMillis = details.lastUsedInMillis, ) } @@ -328,6 +330,7 @@ class SecureStoreBackedAutofillStore @Inject constructor( id = id, domainTitle = domainTitle, lastUpdatedMillis = lastUpdatedMillis, + lastUsedInMillis = lastUsedMillis, ), password = password, notes = notes, diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorage.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorage.kt index dcbd3e9faf22..ff018b1155ad 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorage.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorage.kt @@ -302,6 +302,7 @@ class RealSecureStorage @Inject constructor( notesIv = encryptedNotes?.iv, domainTitle = details.domainTitle, lastUpdatedInMillis = details.lastUpdatedMillis, + lastUsedInMillis = details.lastUsedInMillis, ) } @@ -319,6 +320,7 @@ class RealSecureStorage @Inject constructor( id = id, domainTitle = domainTitle, lastUpdatedMillis = lastUpdatedInMillis, + lastUsedInMillis = lastUsedInMillis, ) // only encrypt when there's data diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorageModels.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorageModels.kt index 9650d40ef567..574b6c2bd68a 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorageModels.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/securestorage/SecureStorageModels.kt @@ -46,6 +46,7 @@ data class WebsiteLoginDetailsWithCredentials( * [id] database id associated to the website login * [domainTitle] title associated to the login * [lastUpdatedMillis] time in milliseconds when the credential was last updated + * [lastUsedInMillis] time in milliseconds when the credential was last used to autofill */ data class WebsiteLoginDetails( val domain: String?, @@ -53,6 +54,7 @@ data class WebsiteLoginDetails( val id: Long? = null, val domainTitle: String? = null, val lastUpdatedMillis: Long? = null, + val lastUsedInMillis: Long? = null, ) { override fun toString(): String { return """ diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/InternalAutofillStore.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/InternalAutofillStore.kt index e6e59d43c7e1..9e30e74b56c6 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/InternalAutofillStore.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/InternalAutofillStore.kt @@ -103,9 +103,11 @@ interface InternalAutofillStore : AutofillStore { /** * Updates the given login credentials, replacing what was saved before for the credentials with the specified ID * @param credentials The ID of the given credentials must match a saved credential for it to be updated. + * @param refreshLastUpdatedTimestamp Whether to update the last-updated timestamp of the credential. + * Defaults to true. Set to false if you don't want the last-updated timestamp modified. * @return The saved credential if it saved successfully, otherwise null */ - suspend fun updateCredentials(credentials: LoginCredentials): LoginCredentials? + suspend fun updateCredentials(credentials: LoginCredentials, refreshLastUpdatedTimestamp: Boolean = true): LoginCredentials? /** * Used to reinsert a credential that was previously deleted diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsGrouper.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsGrouper.kt index cd85663604e3..f0c5c78d2034 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsGrouper.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsGrouper.kt @@ -22,7 +22,10 @@ import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCreden import com.duckduckgo.autofill.impl.urlmatcher.AutofillUrlMatcher import com.duckduckgo.di.scopes.FragmentScope import com.squareup.anvil.annotations.ContributesBinding +import java.util.* import javax.inject.Inject +import javax.inject.Named +import kotlin.Comparator interface AutofillSelectCredentialsGrouper { fun group( @@ -41,6 +44,8 @@ interface AutofillSelectCredentialsGrouper { class RealAutofillSelectCredentialsGrouper @Inject constructor( private val autofillUrlMatcher: AutofillUrlMatcher, private val sorter: CredentialListSorter, + @Named("LastUsedCredentialSorter") private val lastUsedCredentialSorter: TimestampBasedLoginSorter, + @Named("LastUpdatedCredentialSorter") private val lastUpdatedCredentialSorter: TimestampBasedLoginSorter, ) : AutofillSelectCredentialsGrouper { override fun group( @@ -84,19 +89,30 @@ class RealAutofillSelectCredentialsGrouper @Inject constructor( val sortedPartialMatches = groups.partialMatches.toSortedMap(sorter.comparator()) val sortedOtherMatches = groups.shareableCredentials.toSortedMap(sorter.comparator()) - // sort inside each group, where most recently updated is first - val sortedPerfectMatches = groups.perfectMatches.sortedByDescending { it.lastUpdatedMillis } - sortedPartialMatches.forEach { (key, value) -> - val sorted = value.sortedByDescending { it.lastUpdatedMillis } - sortedPartialMatches[key] = sorted - } + // now that headings are sorted, sort inside each group, where the sort order is: + // last used is most important, + // then last modified. + // greater timestamps come first in the sorted list - // sort inside each group, where most recently updated is first - sortedOtherMatches.forEach { (key, value) -> - val sorted = value.sortedByDescending { it.lastUpdatedMillis } - sortedOtherMatches[key] = sorted - } + val sortedPerfectMatches = sortPerfectMatches(groups) + sortImperfectMatches(sortedPartialMatches) + sortImperfectMatches(sortedOtherMatches) return Groups(sortedPerfectMatches, sortedPartialMatches, sortedOtherMatches) } + + private fun sortPerfectMatches(groups: Groups): List { + return groups.perfectMatches.sortedWith(timestampComparator()) + } + + private fun sortImperfectMatches(group: SortedMap>) { + group.forEach { (key, value) -> + val sorted = value.sortedWith(timestampComparator()) + group[key] = sorted + } + } + + private fun timestampComparator(): Comparator { + return lastUsedCredentialSorter.reversed().then(lastUpdatedCredentialSorter.reversed()) + } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsSorter.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsSorter.kt new file mode 100644 index 000000000000..0f97c59e2777 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsSorter.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 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.autofill.impl.ui.credential.selecting + +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import javax.inject.Named + +interface TimestampBasedLoginSorter : Comparator + +@ContributesBinding(FragmentScope::class) +@Named("LastUsedCredentialSorter") +class LastUsedCredentialSorter @Inject constructor() : TimestampBasedLoginSorter { + + /** + * This comparator sorts based on last-used timestamps. + * + * Null timestamps come first, then older timestamps are sorted before newer timestamps. + */ + override fun compare( + o1: LoginCredentials?, + o2: LoginCredentials?, + ): Int { + // handle where one or both of the objects are null + if (o1 == null && o2 == null) return 0 + if (o1 == null) return -1 + if (o2 == null) return 1 + + // handle where one or both of the timestamps are null + val lastUsed1 = o1.lastUsedMillis + val lastUsed2 = o2.lastUsedMillis + + if (lastUsed1 == null && lastUsed2 == null) return 0 + if (lastUsed1 == null) return -1 + if (lastUsed2 == null) return 1 + + return lastUsed1.compareTo(lastUsed2) + } +} + +@ContributesBinding(FragmentScope::class) +@Named("LastUpdatedCredentialSorter") +class LastUpdatedCredentialSorter @Inject constructor() : TimestampBasedLoginSorter { + + /** + * This comparator sorts based on last-updated timestamps. + * + * Null timestamps come first, then older timestamps are sorted before newer timestamps. + */ + override fun compare( + o1: LoginCredentials?, + o2: LoginCredentials?, + ): Int { + // handle where one or both of the objects are null + if (o1 == null && o2 == null) return 0 + if (o1 == null) return -1 + if (o2 == null) return 1 + + // handle where one or both of the timestamps are null + val lastUpdated1 = o1.lastUpdatedMillis + val lastUpdated2 = o2.lastUpdatedMillis + + if (lastUpdated1 == null && lastUpdated2 == null) return 0 + if (lastUpdated1 == null) return -1 + if (lastUpdated2 == null) return 1 + + return lastUpdated1.compareTo(lastUpdated2) + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelection.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelection.kt index 7ab6db3cf4ef..38e6c4a5e3c0 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelection.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelection.kt @@ -31,6 +31,7 @@ import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames +import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding @@ -47,6 +48,7 @@ class ResultHandlerCredentialSelection @Inject constructor( private val pixel: Pixel, private val deviceAuthenticator: DeviceAuthenticator, private val appBuildConfig: AppBuildConfig, + private val autofillStore: InternalAutofillStore, ) : AutofillFragmentResultsPlugin { override fun processResult( @@ -85,6 +87,8 @@ class ResultHandlerCredentialSelection @Inject constructor( val selectedCredentials: LoginCredentials = result.safeGetParcelable(CredentialAutofillPickerDialog.KEY_CREDENTIALS) ?: return + selectedCredentials.updateLastUsedTimestamp() + pixel.fire(AutofillPixelNames.AUTOFILL_AUTHENTICATION_TO_AUTOFILL_SHOWN) withContext(dispatchers.main()) { @@ -114,6 +118,13 @@ class ResultHandlerCredentialSelection @Inject constructor( } } + private fun LoginCredentials.updateLastUsedTimestamp() { + appCoroutineScope.launch(dispatchers.io()) { + val updated = this@updateLastUsedTimestamp.copy(lastUsedMillis = System.currentTimeMillis()) + autofillStore.updateCredentials(updated, refreshLastUpdatedTimestamp = false) + } + } + @Suppress("DEPRECATION") @SuppressLint("NewApi") private inline fun Bundle.safeGetParcelable(key: String) = diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreatorTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreatorTest.kt index 43bb2b17593e..f16fc89e9679 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreatorTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreatorTest.kt @@ -112,11 +112,11 @@ class RealDuckAddressLoginCreatorTest { } private suspend fun verifyLoginSaved() = verify(autofillStore).saveCredentials(eq(URL), any()) - private suspend fun verifyLoginUpdated() = verify(autofillStore).updateCredentials(any()) + private suspend fun verifyLoginUpdated() = verify(autofillStore).updateCredentials(any(), any()) private suspend fun verifyNotSavedOrUpdated() { verify(autofillStore, never()).saveCredentials(any(), any()) - verify(autofillStore, never()).updateCredentials(any()) + verify(autofillStore, never()).updateCredentials(any(), any()) } private fun aLogin(id: Long? = null, username: String? = null, password: String? = null): LoginCredentials { diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt index db6c3598baa8..f18d8ac5d66b 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt @@ -33,6 +33,7 @@ import com.duckduckgo.autofill.impl.urlmatcher.AutofillDomainNameUrlMatcher import com.duckduckgo.autofill.impl.urlmatcher.AutofillUrlMatcher import com.duckduckgo.autofill.store.AutofillPrefsStore import com.duckduckgo.autofill.store.LastUpdatedTimeProvider +import com.duckduckgo.autofill.sync.CredentialsFixtures.toLoginCredentials import com.duckduckgo.autofill.sync.CredentialsSyncMetadata import com.duckduckgo.autofill.sync.SyncCredentialsListener import com.duckduckgo.autofill.sync.inMemoryAutofillDatabase @@ -446,6 +447,22 @@ class SecureStoreBackedAutofillStoreTest { assertEquals(originalCredentials.password, reinsertedCredentials.password) } + @Test + fun whenUpdatingCredentialsByDefaultLastUpdatedTimestampGetsUpdated() = runTest { + setupTesteeWithAutofillAvailable() + val saved = storeCredentials(id = 1, domain = "example.com", username = "username", password = "password") + val updated = testee.updateCredentials(saved)!! + assertEquals(UPDATED_INITIAL_LAST_UPDATED, updated.lastUpdatedMillis) + } + + @Test + fun whenUpdatingCredentialsAndSpecifyNotToUpdateLastUpdatedTimestampThenNotUpdated() = runTest { + setupTesteeWithAutofillAvailable() + val saved = storeCredentials(id = 1, domain = "example.com", username = "username", password = "password") + val updated = testee.updateCredentials(saved, refreshLastUpdatedTimestamp = false)!! + assertEquals(DEFAULT_INITIAL_LAST_UPDATED, updated.lastUpdatedMillis) + } + private fun List.assertHasNoLoginCredentials( url: String, username: String, @@ -519,10 +536,10 @@ class SecureStoreBackedAutofillStoreTest { password: String, lastUpdatedTimeMillis: Long = DEFAULT_INITIAL_LAST_UPDATED, notes: String = "notes", - ) { + ): LoginCredentials { val details = WebsiteLoginDetails(domain = domain, username = username, id = id, lastUpdatedMillis = lastUpdatedTimeMillis) val credentials = WebsiteLoginDetailsWithCredentials(details, password, notes) - secureStore.addWebsiteLoginDetailsWithCredentials(credentials) + return secureStore.addWebsiteLoginDetailsWithCredentials(credentials).toLoginCredentials() } private class FakeSecureStore(val canAccessSecureStorage: Boolean) : SecureStorage { diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/securestorage/RealSecureStorageTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/securestorage/RealSecureStorageTest.kt index a74fb43cee2e..ff14ce7c3950 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/securestorage/RealSecureStorageTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/securestorage/RealSecureStorageTest.kt @@ -73,6 +73,7 @@ class RealSecureStorageTest { notesIv = expectedEncryptedIv, domainTitle = "test", lastUpdatedInMillis = 1000L, + lastUsedInMillis = null, ) @Before diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPasswordTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPasswordTest.kt index e231d6d17b1f..2f4f9a072cf4 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPasswordTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPasswordTest.kt @@ -126,7 +126,7 @@ class ResultHandlerUseGeneratedPasswordTest { ) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) verify(autofillStore, never()).saveCredentials(any(), any()) - verify(autofillStore, never()).updateCredentials(any()) + verify(autofillStore, never()).updateCredentials(any(), any()) } @Test @@ -143,7 +143,7 @@ class ResultHandlerUseGeneratedPasswordTest { ) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) verify(autofillStore, never()).saveCredentials(any(), any()) - verify(autofillStore).updateCredentials(any()) + verify(autofillStore).updateCredentials(any(), any()) } @Test @@ -160,7 +160,7 @@ class ResultHandlerUseGeneratedPasswordTest { ) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) verify(autofillStore, never()).saveCredentials(any(), any()) - verify(autofillStore).updateCredentials(any()) + verify(autofillStore).updateCredentials(any(), any()) } @Test diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/LastUpdatedCredentialSorterTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/LastUpdatedCredentialSorterTest.kt new file mode 100644 index 000000000000..b05ac8a37b93 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/LastUpdatedCredentialSorterTest.kt @@ -0,0 +1,81 @@ +package com.duckduckgo.autofill.impl.ui.credential.selecting + +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import org.junit.Assert.assertEquals +import org.junit.Test + +class LastUpdatedCredentialSorterTest { + + private val testee = LastUpdatedCredentialSorter() + + @Test + fun whenTimestampsAreEqualThen0Returned() { + val login1 = aLogin(lastUpdatedTimestamp = 1) + val login2 = aLogin(lastUpdatedTimestamp = 1) + val result = testee.compare(login1, login2) + assertEquals(0, result) + } + + @Test + fun whenTimestampsAreBothNullThen0Returned() { + val login1 = aLogin(lastUpdatedTimestamp = null) + val login2 = aLogin(lastUpdatedTimestamp = null) + val result = testee.compare(login1, login2) + assertEquals(0, result) + } + + @Test + fun whenLogin1TimestampIsLowerThenSortedBeforeOtherLogin() { + val login1 = aLogin(lastUpdatedTimestamp = 1) + val login2 = aLogin(lastUpdatedTimestamp = 2) + val result = testee.compare(login1, login2) + assertEquals(-1, result) + } + + @Test + fun whenLogin1IsMissingATimestampThenSortedBeforeOtherLogin() { + val login1 = aLogin(lastUpdatedTimestamp = null) + val login2 = aLogin(lastUpdatedTimestamp = 1) + val result = testee.compare(login1, login2) + assertEquals(-1, result) + } + + @Test + fun whenLogin2TimestampIsLowerThenSortedBeforeOtherLogin() { + val login1 = aLogin(lastUpdatedTimestamp = 2) + val login2 = aLogin(lastUpdatedTimestamp = 1) + val result = testee.compare(login1, login2) + assertEquals(1, result) + } + + @Test + fun whenLogin2IsMissingATimestampThenSortedBeforeOtherLogin() { + val login1 = aLogin(lastUpdatedTimestamp = 1) + val login2 = aLogin(lastUpdatedTimestamp = null) + val result = testee.compare(login1, login2) + assertEquals(1, result) + } + + @Test + fun whenLogin1IsNullThenSortedBeforeOtherLogin() { + val login2 = aLogin(lastUpdatedTimestamp = null) + val result = testee.compare(null, login2) + assertEquals(-1, result) + } + + @Test + fun whenLogin2IsNullThenSortedBeforeOtherLogin() { + val login1 = aLogin(lastUpdatedTimestamp = null) + val result = testee.compare(login1, null) + assertEquals(1, result) + } + + @Test + fun whenBothLoginsAreNullThenTreatedAsEquals() { + assertEquals(0, testee.compare(null, null)) + } + + private fun aLogin(lastUpdatedTimestamp: Long?): LoginCredentials { + return LoginCredentials(domain = "example.com", username = "user", password = "pass", lastUpdatedMillis = lastUpdatedTimestamp) + } +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/LastUsedCredentialSorterTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/LastUsedCredentialSorterTest.kt new file mode 100644 index 000000000000..07ce4b40b425 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/LastUsedCredentialSorterTest.kt @@ -0,0 +1,81 @@ +package com.duckduckgo.autofill.impl.ui.credential.selecting + +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import org.junit.Assert.assertEquals +import org.junit.Test + +class LastUsedCredentialSorterTest { + + private val testee = LastUsedCredentialSorter() + + @Test + fun whenTimestampsAreEqualThen0Returned() { + val login1 = aLogin(lastUsedTimestamp = 1) + val login2 = aLogin(lastUsedTimestamp = 1) + val result = testee.compare(login1, login2) + assertEquals(0, result) + } + + @Test + fun whenTimestampsAreBothNullThen0Returned() { + val login1 = aLogin(lastUsedTimestamp = null) + val login2 = aLogin(lastUsedTimestamp = null) + val result = testee.compare(login1, login2) + assertEquals(0, result) + } + + @Test + fun whenLogin1TimestampIsLowerThenSortedBeforeOtherLogin() { + val login1 = aLogin(lastUsedTimestamp = 1) + val login2 = aLogin(lastUsedTimestamp = 2) + val result = testee.compare(login1, login2) + assertEquals(-1, result) + } + + @Test + fun whenLogin1IsMissingATimestampThenSortedBeforeOtherLogin() { + val login1 = aLogin(lastUsedTimestamp = null) + val login2 = aLogin(lastUsedTimestamp = 1) + val result = testee.compare(login1, login2) + assertEquals(-1, result) + } + + @Test + fun whenLogin2TimestampIsLowerThenSortedBeforeOtherLogin() { + val login1 = aLogin(lastUsedTimestamp = 2) + val login2 = aLogin(lastUsedTimestamp = 1) + val result = testee.compare(login1, login2) + assertEquals(1, result) + } + + @Test + fun whenLogin2IsMissingATimestampThenSortedBeforeOtherLogin() { + val login1 = aLogin(lastUsedTimestamp = 1) + val login2 = aLogin(lastUsedTimestamp = null) + val result = testee.compare(login1, login2) + assertEquals(1, result) + } + + @Test + fun whenLogin1IsNullThenSortedBeforeOtherLogin() { + val login2 = aLogin(lastUsedTimestamp = null) + val result = testee.compare(null, login2) + assertEquals(-1, result) + } + + @Test + fun whenLogin2IsNullThenSortedBeforeOtherLogin() { + val login1 = aLogin(lastUsedTimestamp = null) + val result = testee.compare(login1, null) + assertEquals(1, result) + } + + @Test + fun whenBothLoginsAreNullThenTreatedAsEquals() { + assertEquals(0, testee.compare(null, null)) + } + + private fun aLogin(lastUsedTimestamp: Long?): LoginCredentials { + return LoginCredentials(domain = "example.com", username = "user", password = "pass", lastUsedMillis = lastUsedTimestamp) + } +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/RealAutofillSelectCredentialsGrouperTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/RealAutofillSelectCredentialsGrouperTest.kt index e6de6098641c..ea95974f4f59 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/RealAutofillSelectCredentialsGrouperTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/RealAutofillSelectCredentialsGrouperTest.kt @@ -31,10 +31,14 @@ class RealAutofillSelectCredentialsGrouperTest { private val urlMatcher = AutofillDomainNameUrlMatcher(TestUrlUnicodeNormalizer()) private val sorter = CredentialListSorterByTitleAndDomain(urlMatcher) + private val lastUsedSorter = LastUsedCredentialSorter() + private val lastModifiedSorter = LastUpdatedCredentialSorter() private val testee = RealAutofillSelectCredentialsGrouper( autofillUrlMatcher = urlMatcher, sorter = sorter, + lastUsedCredentialSorter = lastUsedSorter, + lastUpdatedCredentialSorter = lastModifiedSorter, ) @Test @@ -100,7 +104,7 @@ class RealAutofillSelectCredentialsGrouperTest { } @Test - fun whenSortingPerfectMatchesThenLastEditedSortedFirst() { + fun whenSortingPerfectMatchesWithNoLastUsedThenLastUpdatedSortedFirst() { val creds = listOf( creds(lastUpdated = 100, domain = "example.com"), creds(lastUpdated = 300, domain = "example.com"), @@ -113,7 +117,20 @@ class RealAutofillSelectCredentialsGrouperTest { } @Test - fun whenSortingPartialMatchesThenLastEditedSortedFirst() { + fun whenSortingPerfectMatchesThenLastUsedSortedFirst() { + val creds = listOf( + creds(lastUsed = 100, domain = "example.com"), + creds(lastUsed = 300, domain = "example.com"), + creds(lastUsed = 200, domain = "example.com"), + ) + val grouped = testee.group("example.com", unsortedCredentials = creds) + assertEquals(300L, grouped.perfectMatches[0].lastUsedMillis) + assertEquals(200L, grouped.perfectMatches[1].lastUsedMillis) + assertEquals(100L, grouped.perfectMatches[2].lastUsedMillis) + } + + @Test + fun whenSortingPartialMatchesWithNoLastUsedThenLastUpdatedSortedFirst() { val creds = listOf( creds(lastUpdated = 100, domain = "foo.example.com"), creds(lastUpdated = 300, domain = "foo.example.com"), @@ -126,6 +143,48 @@ class RealAutofillSelectCredentialsGrouperTest { assertEquals(100L, group?.get(2)?.lastUpdatedMillis) } + @Test + fun whenSortingPartialMatchesThenLastUpdatedSortedFirst() { + val creds = listOf( + creds(lastUsed = 100, domain = "foo.example.com"), + creds(lastUsed = 300, domain = "foo.example.com"), + creds(lastUsed = 200, domain = "foo.example.com"), + ) + val grouped = testee.group("example.com", unsortedCredentials = creds) + val group = grouped.partialMatches["foo.example.com"] + assertEquals(300L, group?.get(0)?.lastUsedMillis) + assertEquals(200L, group?.get(1)?.lastUsedMillis) + assertEquals(100L, group?.get(2)?.lastUsedMillis) + } + + @Test + fun whenSortingOtherMatchesWithNoLastUsedThenLastUpdatedSortedFirst() { + val creds = listOf( + creds(lastUpdated = 100, domain = "other.site"), + creds(lastUpdated = 300, domain = "other.site"), + creds(lastUpdated = 200, domain = "other.site"), + ) + val grouped = testee.group("example.com", unsortedCredentials = creds) + val group = grouped.shareableCredentials["other.site"] + assertEquals(300L, group?.get(0)?.lastUpdatedMillis) + assertEquals(200L, group?.get(1)?.lastUpdatedMillis) + assertEquals(100L, group?.get(2)?.lastUpdatedMillis) + } + + @Test + fun whenSortingOtherMatchesThenLastUsedSortedFirst() { + val creds = listOf( + creds(lastUsed = 100, domain = "other.site"), + creds(lastUsed = 300, domain = "other.site"), + creds(lastUsed = 200, domain = "other.site"), + ) + val grouped = testee.group("example.com", unsortedCredentials = creds) + val group = grouped.shareableCredentials["other.site"] + assertEquals(300L, group?.get(0)?.lastUsedMillis) + assertEquals(200L, group?.get(1)?.lastUsedMillis) + assertEquals(100L, group?.get(2)?.lastUsedMillis) + } + @Test fun whenThereShareableCredentialsThenTheyAreGroupedByDomainLikePartialMatches() { val creds = listOf( @@ -183,7 +242,8 @@ class RealAutofillSelectCredentialsGrouperTest { domain: String, id: Long? = null, lastUpdated: Long? = null, + lastUsed: Long? = null, ): LoginCredentials { - return LoginCredentials(id = id, domain = domain, username = "a", password = "b", lastUpdatedMillis = lastUpdated) + return LoginCredentials(id = id, domain = domain, username = "a", password = "b", lastUpdatedMillis = lastUpdated, lastUsedMillis = lastUsed) } } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelectionTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelectionTest.kt index d3622ec1d555..af92579d5953 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelectionTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelectionTest.kt @@ -28,6 +28,7 @@ import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.NoMatch import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.deviceauth.FakeAuthenticator +import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.test.CoroutineTestRule import kotlinx.coroutines.test.runTest import org.junit.Before @@ -48,6 +49,7 @@ class ResultHandlerCredentialSelectionTest { private val appBuildConfig: AppBuildConfig = mock() private lateinit var deviceAuthenticator: FakeAuthenticator private lateinit var testee: ResultHandlerCredentialSelection + private val autofillStore: InternalAutofillStore = mock() @Before fun setup() = runTest { @@ -156,6 +158,7 @@ class ResultHandlerCredentialSelectionTest { pixel = pixel, deviceAuthenticator = deviceAuthenticator, appBuildConfig = appBuildConfig, + autofillStore = autofillStore, ) } } diff --git a/autofill/autofill-store/src/main/java/com/duckduckgo/securestorage/store/db/SecureStorageDatabase.kt b/autofill/autofill-store/src/main/java/com/duckduckgo/securestorage/store/db/SecureStorageDatabase.kt index 4bf9cbd286be..f4b0be8b633f 100644 --- a/autofill/autofill-store/src/main/java/com/duckduckgo/securestorage/store/db/SecureStorageDatabase.kt +++ b/autofill/autofill-store/src/main/java/com/duckduckgo/securestorage/store/db/SecureStorageDatabase.kt @@ -22,7 +22,7 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase @Database( - version = 4, + version = 5, entities = [ WebsiteLoginCredentialsEntity::class, NeverSavedSiteEntity::class, @@ -54,4 +54,9 @@ val ALL_MIGRATIONS = arrayOf( db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_never_saved_sites_domain` ON `never_saved_sites` (`domain`)") } }, + object : Migration(4, 5) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `website_login_credentials` ADD COLUMN `lastUsedInMillis` INTEGER") + } + }, ) diff --git a/autofill/autofill-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsEntity.kt b/autofill/autofill-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsEntity.kt index d980ecd45040..34f3ad521e33 100644 --- a/autofill/autofill-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsEntity.kt +++ b/autofill/autofill-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsEntity.kt @@ -30,4 +30,5 @@ data class WebsiteLoginCredentialsEntity( val notesIv: String?, val domainTitle: String?, val lastUpdatedInMillis: Long?, + val lastUsedInMillis: Long?, ) diff --git a/autofill/autofill-store/src/test/java/com.duckduckgo.autofill.store/RealSecureStorageRepositoryTest.kt b/autofill/autofill-store/src/test/java/com.duckduckgo.autofill.store/RealSecureStorageRepositoryTest.kt index 1ad0d389e989..f88d22821c2a 100644 --- a/autofill/autofill-store/src/test/java/com.duckduckgo.autofill.store/RealSecureStorageRepositoryTest.kt +++ b/autofill/autofill-store/src/test/java/com.duckduckgo.autofill.store/RealSecureStorageRepositoryTest.kt @@ -203,6 +203,7 @@ class RealSecureStorageRepositoryTest { notesIv: String = "notesIv", domainTitle: String = "test", lastUpdatedInMillis: Long = 0L, + lastUsedInMillis: Long = 0L, ): WebsiteLoginCredentialsEntity { return WebsiteLoginCredentialsEntity( id = id, @@ -214,6 +215,7 @@ class RealSecureStorageRepositoryTest { notesIv = notesIv, domainTitle = domainTitle, lastUpdatedInMillis = lastUpdatedInMillis, + lastUsedInMillis = lastUsedInMillis, ) } } From 752fd904cec81627a9bfc0c7359a7874ef5b3c6d Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Tue, 20 Feb 2024 10:37:34 +0000 Subject: [PATCH 12/19] Add GH workflow to run critical autofill tests each night (#4186) Task/Issue URL: https://app.asana.com/0/0/1206622911703638/f ### Description Adds a nightly workflow to execute the autofill e2e maestro tests. Also refactors the autofill test suite into smaller flows as trying to run one large flow exceeds the Maestro timeout threshold (7 mins). ### Steps to test this PR - [ ] Check the workflow file, and make sure that looks ok to run as a nightly job and alert on failure - [ ] They have been running well in my testing (e.g., [test run](https://console.mobile.dev/uploads/4946a190-1df4-4bb2-bfeb-2d636cd88311?teamId=eba4e11d-655d-4e7f-a936-5b56de0edebb&appId=f2da50d2-9691-44f8-9985-74c4555ff437&analysisId=e2e192bc-d29c-43cc-ba02-fe17045fdf40)), but you can optionally also trigger the Maestro tests manually using the following commands: - `./gradlew -Pautofill-disable-auth-requirement assemblePlayRelease` - `version=$(gr -q getVersionName)` - `maestro cloud --include-tags autofillNoAuthTests "app/build/outputs/apk/play/release/duckduckgo-$version-play-release.apk" .maestro` --- .github/workflows/e2e-nightly-autofill.yml | 76 +++++++++++++++++++ .maestro/autofill/0_all.yaml | 13 +--- .../1_autofill_shown_in_overflow.yaml | 12 +-- ...ofill_add_search_update_delete_creds.yaml} | 23 +++++- .../2_autofill_reach_creds_management.yaml | 9 --- ...ofill_prompted_to_save_creds_on_form.yaml} | 16 ++-- .../{3_script.js => steps/2_script.js} | 0 .../steps/access_passwords_screen.yaml | 9 +++ .../delete_logins.yaml} | 4 +- .../manual_update.yaml} | 0 .../search_logins.yaml} | 0 11 files changed, 127 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/e2e-nightly-autofill.yml rename .maestro/autofill/{3_autofill_manually_add_cred.yaml => 2_autofill_add_search_update_delete_creds.yaml} (74%) delete mode 100644 .maestro/autofill/2_autofill_reach_creds_management.yaml rename .maestro/autofill/{7_autofill_prompted_to_save_creds_on_form.yaml => 3_autofill_prompted_to_save_creds_on_form.yaml} (75%) rename .maestro/autofill/{3_script.js => steps/2_script.js} (100%) create mode 100644 .maestro/autofill/steps/access_passwords_screen.yaml rename .maestro/autofill/{6_delete_logins.yaml => steps/delete_logins.yaml} (90%) rename .maestro/autofill/{5_autofill_manually_updating_an_existing_credential.yaml => steps/manual_update.yaml} (100%) rename .maestro/autofill/{4_search_logins.yaml => steps/search_logins.yaml} (100%) diff --git a/.github/workflows/e2e-nightly-autofill.yml b/.github/workflows/e2e-nightly-autofill.yml new file mode 100644 index 000000000000..c2b590eb28f8 --- /dev/null +++ b/.github/workflows/e2e-nightly-autofill.yml @@ -0,0 +1,76 @@ +name: Autofill Feature Critical Path End-to-End tests + +on: + schedule: + - cron: '30 6 * * *' # run at 6.30 AM UTC + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + instrumentation_tests: + runs-on: ubuntu-latest + name: Autofill Critical Path End-to-End Tests + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + + - name: Create folder + if: always() + run: mkdir apk + + - name: Decode keys + uses: davidSchuppa/base64Secret-toFile-action@v2 + with: + secret: ${{ secrets.FAKE_RELEASE_PROPERTIES }} + fileName: ddg_android_build.properties + destination-path: $HOME/jenkins_static/com.duckduckgo.mobile.android/ + + - name: Decode key file + uses: davidSchuppa/base64Secret-toFile-action@v2 + with: + secret: ${{ secrets.FAKE_RELEASE_KEY }} + fileName: android + destination-path: $HOME/jenkins_static/com.duckduckgo.mobile.android/ + + - name: Assemble APK which does not require auth to use Autofill + uses: gradle/gradle-build-action@v2 + with: + arguments: assemblePlayRelease -Pautofill-disable-auth-requirement -Pforce-default-variant -x lint + + - name: Move APK to new folder + if: always() + run: find . -name "*.apk" -exec mv '{}' apk/release.apk \; + + - name: Autofill Critical Path E2E Flows + uses: mobile-dev-inc/action-maestro-cloud@v1.6.0 + with: + api-key: ${{ secrets.MOBILE_DEV_API_KEY }} + name: ${{ github.sha }} + app-file: apk/release.apk + android-api-level: 30 + workspace: .maestro + include-tags: autofillNoAuthTests + + - name: Create Asana task when workflow failed + if: ${{ failure() }} + id: create-failure-task + uses: duckduckgo/native-github-asana-sync@v1.1 + with: + asana-pat: ${{ secrets.GH_ASANA_SECRET }} + asana-project: ${{ vars.GH_ANDROID_APP_PROJECT_ID }} + asana-section: ${{ vars.GH_ANDROID_APP_INCOMING_SECTION_ID }} + asana-task-name: GH Workflow Failure - Autofill Critical Path E2E Flows + asana-task-description: Autofill critical path tests have failed. See https://github.com/duckduckgo/Android/actions/runs/${{ github.run_id }} + action: 'create-asana-task' \ No newline at end of file diff --git a/.maestro/autofill/0_all.yaml b/.maestro/autofill/0_all.yaml index 0c5fd4b85e27..cd46b59351b2 100644 --- a/.maestro/autofill/0_all.yaml +++ b/.maestro/autofill/0_all.yaml @@ -1,19 +1,12 @@ appId: com.duckduckgo.mobile.android name: "Autofill: Run all tests" -tags: - - autofillNoAuthTests --- # Pre-requisite: the app is installed on an autofill-eligible device with a special build flag set to bypass device-authentication requirement +# This should only be run locally, as Maestro cloud will likely timeout trying to run them all as one flow. - launchApp: clearState: true - runFlow: ../shared/onboarding.yaml - runFlow: 1_autofill_shown_in_overflow.yaml - - # Everything below requires a device has device-level authentication set (e.g., a PIN/Password etc...) -- runFlow: 2_autofill_reach_creds_management.yaml -- runFlow: 3_autofill_manually_add_cred.yaml -- runFlow: 4_search_logins.yaml -- runFlow: 5_autofill_manually_updating_an_existing_credential.yaml -- runFlow: 6_delete_logins.yaml -- runFlow: 7_autofill_prompted_to_save_creds_on_form.yaml +- runFlow: 2_autofill_add_search_update_delete_creds.yaml +- runFlow: 3_autofill_prompted_to_save_creds_on_form.yaml \ No newline at end of file diff --git a/.maestro/autofill/1_autofill_shown_in_overflow.yaml b/.maestro/autofill/1_autofill_shown_in_overflow.yaml index 5a5544a6ce0c..ee5e95b38629 100644 --- a/.maestro/autofill/1_autofill_shown_in_overflow.yaml +++ b/.maestro/autofill/1_autofill_shown_in_overflow.yaml @@ -1,9 +1,11 @@ appId: com.duckduckgo.mobile.android name: "Autofill: Autofill screen is reachable from overflow menu" +tags: + - autofillNoAuthTests --- -# Pre-requisite: the user has cleared onboarding and is on the new tab page +# Pre-requisite: None (can be run whether auth is required or not) -- tapOn: - id: "com.duckduckgo.mobile.android:id/browserMenuImageView" -- assertVisible: "Passwords" -- tapOn: "Passwords" \ No newline at end of file +- launchApp: + clearState: true +- runFlow: ../shared/onboarding.yaml +- runFlow: steps/access_passwords_screen.yaml \ No newline at end of file diff --git a/.maestro/autofill/3_autofill_manually_add_cred.yaml b/.maestro/autofill/2_autofill_add_search_update_delete_creds.yaml similarity index 74% rename from .maestro/autofill/3_autofill_manually_add_cred.yaml rename to .maestro/autofill/2_autofill_add_search_update_delete_creds.yaml index 7ffab13246e3..3ef054cce02b 100644 --- a/.maestro/autofill/3_autofill_manually_add_cred.yaml +++ b/.maestro/autofill/2_autofill_add_search_update_delete_creds.yaml @@ -1,9 +1,21 @@ appId: com.duckduckgo.mobile.android name: "Autofill: Manually add credentials" +tags: + - autofillNoAuthTests --- -# Pre-requisite: the user is viewing the password manager screen with no saved passwords, on an autofill-eligible device +# Pre-requisite: on an autofill-eligible device -- runScript: 3_script.js +- launchApp: + clearState: true +- runFlow: ../shared/onboarding.yaml +- runFlow: steps/access_passwords_screen.yaml + +- assertVisible: + text: "No passwords saved yet" +- assertNotVisible: + id: searchLogins + +- runScript: steps/2_script.js - repeat: while: @@ -60,4 +72,9 @@ name: "Autofill: Manually add credentials" text: "fill.dev" - assertNotVisible: - text: "fill.dev/example" \ No newline at end of file + text: "fill.dev/example" + + +- runFlow: steps/search_logins.yaml +- runFlow: steps/manual_update.yaml +- runFlow: steps/delete_logins.yaml \ No newline at end of file diff --git a/.maestro/autofill/2_autofill_reach_creds_management.yaml b/.maestro/autofill/2_autofill_reach_creds_management.yaml deleted file mode 100644 index 6c9f78cdb1c7..000000000000 --- a/.maestro/autofill/2_autofill_reach_creds_management.yaml +++ /dev/null @@ -1,9 +0,0 @@ -appId: com.duckduckgo.mobile.android -name: "Autofill: Password manager screen when no passwords saved" ---- -# Pre-requisite: the user is viewing the password manager screen with no saved passwords, on an autofill-eligible device - -- assertVisible: - text: "No passwords saved yet" -- assertNotVisible: - id: searchLogins diff --git a/.maestro/autofill/7_autofill_prompted_to_save_creds_on_form.yaml b/.maestro/autofill/3_autofill_prompted_to_save_creds_on_form.yaml similarity index 75% rename from .maestro/autofill/7_autofill_prompted_to_save_creds_on_form.yaml rename to .maestro/autofill/3_autofill_prompted_to_save_creds_on_form.yaml index 55186a5709e3..224e1bde0db3 100644 --- a/.maestro/autofill/7_autofill_prompted_to_save_creds_on_form.yaml +++ b/.maestro/autofill/3_autofill_prompted_to_save_creds_on_form.yaml @@ -1,10 +1,16 @@ appId: com.duckduckgo.mobile.android name: "Autofill: Prompted to save and update credentials on web form" +tags: + - autofillNoAuthTests --- -# Pre-requisite: the user is viewing the new tab screen, on an autofill-eligible device +# Pre-requisite: on an autofill-eligible device + +- launchApp: + clearState: true +- runFlow: ../shared/onboarding.yaml - tapOn: - text: "search or type URL" + id: "omnibarTextInput" - eraseText - inputText: "fill.dev/form/login-simple" - pressKey: enter @@ -32,9 +38,9 @@ name: "Autofill: Prompted to save and update credentials on web form" text: "Login" - assertVisible: "Update Password" - tapOn: "Update Password" -- tapOn: - id: "browserMenu" -- tapOn: "Passwords" + +- runFlow: steps/access_passwords_screen.yaml + - tapOn: "user" - tapOn: id: "internal_password_icon" diff --git a/.maestro/autofill/3_script.js b/.maestro/autofill/steps/2_script.js similarity index 100% rename from .maestro/autofill/3_script.js rename to .maestro/autofill/steps/2_script.js diff --git a/.maestro/autofill/steps/access_passwords_screen.yaml b/.maestro/autofill/steps/access_passwords_screen.yaml new file mode 100644 index 000000000000..5a5544a6ce0c --- /dev/null +++ b/.maestro/autofill/steps/access_passwords_screen.yaml @@ -0,0 +1,9 @@ +appId: com.duckduckgo.mobile.android +name: "Autofill: Autofill screen is reachable from overflow menu" +--- +# Pre-requisite: the user has cleared onboarding and is on the new tab page + +- tapOn: + id: "com.duckduckgo.mobile.android:id/browserMenuImageView" +- assertVisible: "Passwords" +- tapOn: "Passwords" \ No newline at end of file diff --git a/.maestro/autofill/6_delete_logins.yaml b/.maestro/autofill/steps/delete_logins.yaml similarity index 90% rename from .maestro/autofill/6_delete_logins.yaml rename to .maestro/autofill/steps/delete_logins.yaml index 43401ca10ae4..2c98a0251696 100644 --- a/.maestro/autofill/6_delete_logins.yaml +++ b/.maestro/autofill/steps/delete_logins.yaml @@ -25,6 +25,4 @@ name: "Autofill: Delete credentials" - tapOn: "Delete" - assertVisible: - text: "No passwords saved yet" - -- tapOn: "Navigate up" \ No newline at end of file + text: "No passwords saved yet" \ No newline at end of file diff --git a/.maestro/autofill/5_autofill_manually_updating_an_existing_credential.yaml b/.maestro/autofill/steps/manual_update.yaml similarity index 100% rename from .maestro/autofill/5_autofill_manually_updating_an_existing_credential.yaml rename to .maestro/autofill/steps/manual_update.yaml diff --git a/.maestro/autofill/4_search_logins.yaml b/.maestro/autofill/steps/search_logins.yaml similarity index 100% rename from .maestro/autofill/4_search_logins.yaml rename to .maestro/autofill/steps/search_logins.yaml From 2e5e8cd18a71827ed231cca28c6cda7f6c2a58fc Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Tue, 20 Feb 2024 14:31:58 +0000 Subject: [PATCH 13/19] Re-establish hardware check reporting for autofill capabilities (#4190) Task/Issue URL: https://app.asana.com/0/488551667048375/1206638124888251/f ### Description Re-establishes the hardware check for Autofill device capabilities (including the encrypted storage check). This was removed previously as it was assumed the values were unlikely to change over time, but recently seen an increase in feedback suggesting a newer device (Samsung S24) is hitting this scenario indicating it is something that can and will change over time. ### Steps to test this PR Logcat Filter: `package:mine message~:"capabilit"` - [ ] Fresh install and launch the app - [ ] Verify that you see `Autofill capability pixel fired` - [ ] Background / restore to foreground; verify no new logs about the capability detector - [ ] Kill the app and re-launch it; verify capability detector reports `Already determined device autofill capabilities previously` and does nothing else --- .../pixel/AutofillUniquePixelSenderTest.kt | 104 ++++++++++++++++++ .../pixel/AutofillDeviceCapabilityReporter.kt | 72 ++++++++++++ .../autofill/impl/pixel/AutofillPixelNames.kt | 8 ++ .../impl/pixel/AutofillPixelSender.kt | 104 ++++++++++++++++++ 4 files changed, 288 insertions(+) create mode 100644 app/src/test/java/com/duckduckgo/autofill/pixel/AutofillUniquePixelSenderTest.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillDeviceCapabilityReporter.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelSender.kt diff --git a/app/src/test/java/com/duckduckgo/autofill/pixel/AutofillUniquePixelSenderTest.kt b/app/src/test/java/com/duckduckgo/autofill/pixel/AutofillUniquePixelSenderTest.kt new file mode 100644 index 000000000000..14e0141e44c5 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/autofill/pixel/AutofillUniquePixelSenderTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 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.autofill.pixel + +import android.content.Context +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DEVICE_CAPABILITY_CAPABLE +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DEVICE_CAPABILITY_DEVICE_AUTH_DISABLED +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DEVICE_CAPABILITY_SECURE_STORAGE_UNAVAILABLE +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DEVICE_CAPABILITY_SECURE_STORAGE_UNAVAILABLE_AND_DEVICE_AUTH_DISABLED +import com.duckduckgo.autofill.impl.pixel.AutofillUniquePixelSender +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.test.api.InMemorySharedPreferences +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class AutofillUniquePixelSenderTest { + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val mockPixel: Pixel = mock() + private val context: Context = mock() + private val fakePreferences = InMemorySharedPreferences() + + private val testee = AutofillUniquePixelSender( + pixel = mockPixel, + context = context, + appCoroutineScope = TestScope(), + dispatchers = coroutineTestRule.testDispatcherProvider, + ) + + @Before + fun before() { + whenever(context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE)).thenReturn(fakePreferences) + } + + @Test + fun whenSharedPreferencesHasNoValueThenReturnsFalse() = runTest { + configureSharePreferencesMissingKey() + assertFalse(testee.hasDeterminedCapabilities()) + } + + @Test + fun whenPixelSentThenSharedPreferencesRecordsPixelWasSentBefore() = runTest { + testee.sendCapabilitiesPixel(secureStorageAvailable = true, deviceAuthAvailable = true) + assertTrue(testee.hasDeterminedCapabilities()) + } + + @Test + fun whenSecureStorageIsAvailableAndDeviceAuthEnabledThenSendCorrectPixel() { + testee.sendCapabilitiesPixel(secureStorageAvailable = true, deviceAuthAvailable = true) + verify(mockPixel).fire(AUTOFILL_DEVICE_CAPABILITY_CAPABLE) + } + + @Test + fun whenSecureStorageIsAvailableButDeviceAuthDisabledThenSendCorrectPixel() { + testee.sendCapabilitiesPixel(secureStorageAvailable = true, deviceAuthAvailable = false) + verify(mockPixel).fire(AUTOFILL_DEVICE_CAPABILITY_DEVICE_AUTH_DISABLED) + } + + @Test + fun whenDeviceAuthEnabledButSecureStorageIsNotAvailableThenSendCorrectPixel() { + testee.sendCapabilitiesPixel(secureStorageAvailable = false, deviceAuthAvailable = true) + verify(mockPixel).fire(AUTOFILL_DEVICE_CAPABILITY_SECURE_STORAGE_UNAVAILABLE) + } + + @Test + fun whenDeviceAuthDisabledAndSecureStorageIsNotAvailableThenSendCorrectPixel() { + testee.sendCapabilitiesPixel(secureStorageAvailable = false, deviceAuthAvailable = false) + verify(mockPixel).fire(AUTOFILL_DEVICE_CAPABILITY_SECURE_STORAGE_UNAVAILABLE_AND_DEVICE_AUTH_DISABLED) + } + + private fun configureSharePreferencesMissingKey() { + fakePreferences.remove(KEY_CAPABILITIES_DETERMINED) + } + + companion object { + private const val SHARED_PREFS_FILE = "com.duckduckgo.autofill.pixel.AutofillPixelSender" + private const val KEY_CAPABILITIES_DETERMINED = "KEY_CAPABILITIES_DETERMINED" + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillDeviceCapabilityReporter.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillDeviceCapabilityReporter.kt new file mode 100644 index 000000000000..f309b4aaf419 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillDeviceCapabilityReporter.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 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.autofill.impl.pixel + +import androidx.annotation.UiThread +import androidx.lifecycle.LifecycleOwner +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver +import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator +import com.duckduckgo.autofill.impl.securestorage.SecureStorage +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@ContributesMultibinding( + scope = AppScope::class, + boundType = MainProcessLifecycleObserver::class, +) +class AutofillDeviceCapabilityReporter @Inject constructor( + private val pixel: AutofillPixelSender, + private val secureStorage: SecureStorage, + private val dispatchers: DispatcherProvider, + private val deviceAuthenticator: DeviceAuthenticator, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, +) : MainProcessLifecycleObserver { + + @UiThread + override fun onCreate(owner: LifecycleOwner) { + Timber.v("Autofill device capability reporter created") + + appCoroutineScope.launch(dispatchers.io()) { + if (pixel.hasDeterminedCapabilities()) { + Timber.v("Already determined device autofill capabilities previously") + return@launch + } + + try { + val secureStorageAvailable = secureStorage.canAccessSecureStorage() + val deviceAuthAvailable = deviceAuthenticator.hasValidDeviceAuthentication() + + Timber.d( + "Autofill device capabilities:" + + "\nSecure storage available: $secureStorageAvailable" + + "\nDevice auth available: $deviceAuthAvailable", + ) + + pixel.sendCapabilitiesPixel(secureStorageAvailable, deviceAuthAvailable) + } catch (e: Error) { + Timber.w(e, "Failed to determine device autofill capabilities") + pixel.sendCapabilitiesUndeterminablePixel() + } + } + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt index 32746765fe33..86a4eb783c84 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt @@ -87,6 +87,14 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_CONFIRMATION_PROMPT_DISMISSED("m_autofill_settings_reset_excluded_dismissed"), AUTOFILL_OVERLAPPING_DIALOG("m_autofill_overlapping_dialog"), + + AUTOFILL_DEVICE_CAPABILITY_CAPABLE("m_autofill_device_capability_capable"), + AUTOFILL_DEVICE_CAPABILITY_SECURE_STORAGE_UNAVAILABLE("m_autofill_device_capability_secure_storage_unavailable"), + AUTOFILL_DEVICE_CAPABILITY_DEVICE_AUTH_DISABLED("m_autofill_device_capability_device_auth_disabled"), + AUTOFILL_DEVICE_CAPABILITY_SECURE_STORAGE_UNAVAILABLE_AND_DEVICE_AUTH_DISABLED( + "m_autofill_device_capability_secure_storage_unavailable_and_device_auth_disabled", + ), + AUTOFILL_DEVICE_CAPABILITY_UNKNOWN_ERROR("m_autofill_device_capability_unknown"), } @ContributesMultibinding( diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelSender.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelSender.kt new file mode 100644 index 000000000000..b5cbd83127dd --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelSender.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 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.autofill.impl.pixel + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DEVICE_CAPABILITY_CAPABLE +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DEVICE_CAPABILITY_DEVICE_AUTH_DISABLED +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DEVICE_CAPABILITY_SECURE_STORAGE_UNAVAILABLE +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DEVICE_CAPABILITY_SECURE_STORAGE_UNAVAILABLE_AND_DEVICE_AUTH_DISABLED +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DEVICE_CAPABILITY_UNKNOWN_ERROR +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.launch +import timber.log.Timber + +interface AutofillPixelSender { + suspend fun hasDeterminedCapabilities(): Boolean + fun sendCapabilitiesPixel( + secureStorageAvailable: Boolean, + deviceAuthAvailable: Boolean, + ) + + fun sendCapabilitiesUndeterminablePixel() +} + +@ContributesBinding(AppScope::class) +@SingleInstanceIn(AppScope::class) +class AutofillUniquePixelSender @Inject constructor( + private val pixel: Pixel, + private val context: Context, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, +) : AutofillPixelSender { + + val preferences: SharedPreferences + get() = context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE) + + override suspend fun hasDeterminedCapabilities(): Boolean { + return preferences.getBoolean(KEY_CAPABILITIES_DETERMINED, false) + } + + override fun sendCapabilitiesPixel( + secureStorageAvailable: Boolean, + deviceAuthAvailable: Boolean, + ) { + appCoroutineScope.launch(dispatchers.io()) { + sendPixel(secureStorageAvailable, deviceAuthAvailable).let { + Timber.v("Autofill capability pixel fired: %s", it) + } + preferences.edit { putBoolean(KEY_CAPABILITIES_DETERMINED, true) } + } + } + + override fun sendCapabilitiesUndeterminablePixel() { + appCoroutineScope.launch(dispatchers.io()) { + pixel.fire(AUTOFILL_DEVICE_CAPABILITY_UNKNOWN_ERROR) + preferences.edit { putBoolean(KEY_CAPABILITIES_DETERMINED, true) } + } + } + + private fun sendPixel( + secureStorageAvailable: Boolean, + deviceAuthAvailable: Boolean, + ): AutofillPixelNames { + val pixelName = if (secureStorageAvailable && deviceAuthAvailable) { + AUTOFILL_DEVICE_CAPABILITY_CAPABLE + } else if (!secureStorageAvailable && !deviceAuthAvailable) { + AUTOFILL_DEVICE_CAPABILITY_SECURE_STORAGE_UNAVAILABLE_AND_DEVICE_AUTH_DISABLED + } else if (!deviceAuthAvailable) { + AUTOFILL_DEVICE_CAPABILITY_DEVICE_AUTH_DISABLED + } else { + AUTOFILL_DEVICE_CAPABILITY_SECURE_STORAGE_UNAVAILABLE + } + pixel.fire(pixelName) + return pixelName + } + + companion object { + private const val SHARED_PREFS_FILE = "com.duckduckgo.autofill.pixel.AutofillPixelSender" + private const val KEY_CAPABILITIES_DETERMINED = "KEY_CAPABILITIES_DETERMINED" + } +} From 53f2ea0a55c9ff1bbb428c1cb8c4a5d966b47a27 Mon Sep 17 00:00:00 2001 From: Aitor Viana Date: Wed, 21 Feb 2024 09:28:59 +0000 Subject: [PATCH 14/19] Enforce declaredMethods order when evaluating FFs (#4193) Task/Issue URL: https://app.asana.com/0/488551667048375/1206628523510268/f ### Description See [task](https://app.asana.com/0/488551667048375/1206628523510268/f) for full context explanation ### Steps to test this PR _Test_ - [x] install from this branch and launch the app - [x] go to app settings -> developer settings -> override privacy remote config URL - [x] toggle "use custom URL" on and enter `https://jsonblob.com/api/jsonBlob/1126094951384629248` - [x] tap on force reloading - [x] Use AS (or Flipper) to see the contents of the share prefs `com.duckduckgo.feature.toggle.androidBrowserConfig` - [x] take note of the `hash` -- repeat 3 times -- - [x] force close the app and re-launch - [x] Use AS (or Flipper) to see the contents of the share prefs `com.duckduckgo.feature.toggle.androidBrowserConfig` - [x] verify the `hash` is the same -- END repeat 3 times -- - [x] take note of the sate (enabled/disabled) of the `androidBrowserConfig` sub-features - [x] Go to https://www.jsonblob.com/1126094951384629248 and modify the `hash` of `androidBrowserConfig` - [x] force close the app and re-launch - [x] verify the `hash` is the same - [x] Go to https://www.jsonblob.com/1126094951384629248 and increase the version - [x] force close the app and re-launch - [x] verify the `hash` is different this time, and take note of it - [x] verify the state of the sub-features has not change - [x] Apply the following patch and rebuild / re-install / re-launch the app ```kotlin Index: app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt b/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt --- a/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt (revision 5fe8bd5216708fbbe945832377600bc90233eb24) +++ b/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt (date 1707729280159) @@ -59,4 +59,7 @@ */ @Toggle.DefaultValue(false) fun optimizeTrackerEvaluation(): Toggle + + @Toggle.DefaultValue(false) + fun testFlag(): Toggle } ``` - [x] verify the `androidBrowserConfig_testFlag` is now present in the `com.duckduckgo.feature.toggle.androidBrowserConfig` and its `enable` value is most likely `true` (`rolloutThreshold` < `98`) - [x] verify the `hash` has changed --- .../anvil/compiler/ContributesRemoteFeatureCodeGenerator.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler/ContributesRemoteFeatureCodeGenerator.kt b/anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler/ContributesRemoteFeatureCodeGenerator.kt index 1fe1a6ba09b1..6eddd36ace75 100644 --- a/anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler/ContributesRemoteFeatureCodeGenerator.kt +++ b/anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler/ContributesRemoteFeatureCodeGenerator.kt @@ -418,6 +418,7 @@ class ContributesRemoteFeatureCodeGenerator : CodeGenerator { val concatMethodNames = this.feature.get().javaClass .declaredMethods .map { it.name } + .sorted() .joinToString(separator = "") val hash = %T().writeUtf8(concatMethodNames).md5().hex() return hash From d17a09ceec2dce30c3541273681e658a23f62035 Mon Sep 17 00:00:00 2001 From: Karl Dimla Date: Wed, 21 Feb 2024 12:39:57 +0100 Subject: [PATCH 15/19] Fix unable to refresh keypair (#4198) Task/Issue URL: https://app.asana.com/0/488551667048375/1206654529735313/f ### Description See attached task description ### Steps to test this PR https://app.asana.com/0/0/1206655219268367/f --- .../api/NetworkProtectionState.kt | 11 ++++++++++ .../NetworkProtectionManagementViewModel.kt | 8 +++---- .../impl/state/NetworkProtectionStateImpl.kt | 13 ++++++++++++ ...etworkProtectionManagementViewModelTest.kt | 21 +++++++------------ 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/network-protection/network-protection-api/src/main/java/com/duckduckgo/networkprotection/api/NetworkProtectionState.kt b/network-protection/network-protection-api/src/main/java/com/duckduckgo/networkprotection/api/NetworkProtectionState.kt index db72dad95403..a745de8f0e57 100644 --- a/network-protection/network-protection-api/src/main/java/com/duckduckgo/networkprotection/api/NetworkProtectionState.kt +++ b/network-protection/network-protection-api/src/main/java/com/duckduckgo/networkprotection/api/NetworkProtectionState.kt @@ -40,6 +40,11 @@ interface NetworkProtectionState { */ suspend fun isRunning(): Boolean + /** + * This method will start the Network Protection feature + */ + fun start() + /** * This method will restart the Network Protection feature by disabling it and re-enabling back again */ @@ -56,6 +61,12 @@ interface NetworkProtectionState { */ suspend fun stop() + /** + * This method is the same as [stop] but it also clears the existing VPN reconfiguration, forcing a new registration + * process with the VPN backend + */ + fun clearVPNConfigurationAndStop() + /** * This method returns the current server location Network Protection is routing device's data through. * @return Returns the server location if available, otherwise null. diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementViewModel.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementViewModel.kt index aa3e927e0485..337dc59f157c 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementViewModel.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementViewModel.kt @@ -27,7 +27,6 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.common.utils.ConflatedJob import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ActivityScope -import com.duckduckgo.mobile.android.vpn.VpnFeaturesRegistry import com.duckduckgo.mobile.android.vpn.network.ExternalVpnDetector import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.AlwaysOnState @@ -41,6 +40,7 @@ import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.ERR import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.REVOKED import com.duckduckgo.mobile.android.vpn.ui.AppBreakageCategory import com.duckduckgo.mobile.android.vpn.ui.OpenVpnBreakageCategoryWithBrokenApp +import com.duckduckgo.networkprotection.api.NetworkProtectionState import com.duckduckgo.networkprotection.impl.NetPVpnFeature import com.duckduckgo.networkprotection.impl.configuration.WgTunnelConfig import com.duckduckgo.networkprotection.impl.configuration.asServerDetails @@ -71,13 +71,13 @@ import kotlinx.coroutines.withContext @ContributesViewModel(ActivityScope::class) class NetworkProtectionManagementViewModel @Inject constructor( private val vpnStateMonitor: VpnStateMonitor, - private val featuresRegistry: VpnFeaturesRegistry, private val networkProtectionRepository: NetworkProtectionRepository, private val wgTunnelConfig: WgTunnelConfig, private val dispatcherProvider: DispatcherProvider, private val externalVpnDetector: ExternalVpnDetector, private val networkProtectionPixels: NetworkProtectionPixels, @NetpBreakageCategories private val netpBreakageCategories: List, + private val networkProtectionState: NetworkProtectionState, ) : ViewModel(), DefaultLifecycleObserver { private val refreshVpnRunningState = MutableStateFlow(System.currentTimeMillis()) @@ -239,7 +239,7 @@ class NetworkProtectionManagementViewModel @Inject constructor( fun onStartVpn() { viewModelScope.launch(dispatcherProvider.io()) { - featuresRegistry.registerFeature(NetPVpnFeature.NETP_VPN) + networkProtectionState.start() networkProtectionRepository.enabledTimeInMillis = -1L forceUpdateRunningState() tryShowAlwaysOnPromotion() @@ -287,7 +287,7 @@ class NetworkProtectionManagementViewModel @Inject constructor( private fun onStopVpn() { viewModelScope.launch(dispatcherProvider.io()) { - featuresRegistry.unregisterFeature(NetPVpnFeature.NETP_VPN) + networkProtectionState.clearVPNConfigurationAndStop() forceUpdateRunningState() } } diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/state/NetworkProtectionStateImpl.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/state/NetworkProtectionStateImpl.kt index 0a10f0374c3d..a618b4625726 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/state/NetworkProtectionStateImpl.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/state/NetworkProtectionStateImpl.kt @@ -62,6 +62,12 @@ class NetworkProtectionStateImpl @Inject constructor( return vpnFeaturesRegistry.isFeatureRunning(NetPVpnFeature.NETP_VPN) } + override fun start() { + coroutineScope.launch(dispatcherProvider.io()) { + vpnFeaturesRegistry.registerFeature(NetPVpnFeature.NETP_VPN) + } + } + override fun restart() { coroutineScope.launch(dispatcherProvider.io()) { vpnFeaturesRegistry.refreshFeature(NetPVpnFeature.NETP_VPN) @@ -79,6 +85,13 @@ class NetworkProtectionStateImpl @Inject constructor( vpnFeaturesRegistry.unregisterFeature(NetPVpnFeature.NETP_VPN) } + override fun clearVPNConfigurationAndStop() { + coroutineScope.launch(dispatcherProvider.io()) { + wgTunnelConfig.clearWgConfig() + stop() + } + } + override fun serverLocation(): String? { return runBlocking { wgTunnelConfig.getWgConfig() }?.asServerDetails()?.location } diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementViewModelTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementViewModelTest.kt index 345de5e8a1bc..fc561f658174 100644 --- a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementViewModelTest.kt +++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/management/NetworkProtectionManagementViewModelTest.kt @@ -19,8 +19,6 @@ package com.duckduckgo.networkprotection.impl.management import android.content.Intent import app.cash.turbine.test import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.mobile.android.vpn.FakeVpnFeaturesRegistry -import com.duckduckgo.mobile.android.vpn.VpnFeaturesRegistry import com.duckduckgo.mobile.android.vpn.network.ExternalVpnDetector import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.AlwaysOnState @@ -32,6 +30,7 @@ import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.REV import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.UNKNOWN import com.duckduckgo.mobile.android.vpn.ui.AppBreakageCategory import com.duckduckgo.mobile.android.vpn.ui.OpenVpnBreakageCategoryWithBrokenApp +import com.duckduckgo.networkprotection.api.NetworkProtectionState import com.duckduckgo.networkprotection.impl.NetPVpnFeature import com.duckduckgo.networkprotection.impl.configuration.WgTunnelConfig import com.duckduckgo.networkprotection.impl.management.NetworkProtectionManagementViewModel.AlertState.None @@ -60,8 +59,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -78,8 +75,6 @@ class NetworkProtectionManagementViewModelTest { @Mock private lateinit var vpnStateMonitor: VpnStateMonitor - private lateinit var vpnFeaturesRegistry: VpnFeaturesRegistry - @Mock private lateinit var networkProtectionRepository: NetworkProtectionRepository @@ -92,6 +87,9 @@ class NetworkProtectionManagementViewModelTest { @Mock private lateinit var networkProtectionPixels: NetworkProtectionPixels + @Mock + private lateinit var networkProtectionState: NetworkProtectionState + private val wgQuickConfig = """ [Interface] Address = 10.237.97.63/32 @@ -114,7 +112,6 @@ class NetworkProtectionManagementViewModelTest { @Before fun setUp() { MockitoAnnotations.openMocks(this) - vpnFeaturesRegistry = FakeVpnFeaturesRegistry() runTest { whenever(vpnStateMonitor.isAlwaysOnEnabled()).thenReturn(false) @@ -123,13 +120,13 @@ class NetworkProtectionManagementViewModelTest { testee = NetworkProtectionManagementViewModel( vpnStateMonitor, - vpnFeaturesRegistry, networkProtectionRepository, wgTunnelConfig, coroutineRule.testDispatcherProvider, externalVpnDetector, networkProtectionPixels, testbreakageCategories, + networkProtectionState, ) } @@ -146,20 +143,16 @@ class NetworkProtectionManagementViewModelTest { @Test fun whenOnStartVpnThenRegisterFeature() = runTest { - assertFalse(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)) - testee.onStartVpn() - assertTrue(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)) + verify(networkProtectionState).start() } @Test fun whenOnNetpToggleClickedToDisabledThenUnregisterFeature() = runTest { - vpnFeaturesRegistry.registerFeature(NetPVpnFeature.NETP_VPN) - testee.onNetpToggleClicked(false) - assertFalse(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)) + verify(networkProtectionState).clearVPNConfigurationAndStop() } @Test From 908ae21b21562130de5d0674cdfd096aa020226b Mon Sep 17 00:00:00 2001 From: Josh Leibstein Date: Wed, 21 Feb 2024 12:57:10 +0000 Subject: [PATCH 16/19] Remove SDK checks from AppLinksLauncher (#4200) --- .../app/browser/applinks/AppLinksLauncher.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksLauncher.kt b/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksLauncher.kt index c0cb7b014920..777af6bc68d9 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksLauncher.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/applinks/AppLinksLauncher.kt @@ -20,14 +20,11 @@ import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context import android.content.Intent -import android.os.Build import android.widget.Toast -import androidx.annotation.RequiresApi import androidx.annotation.StringRes import com.duckduckgo.app.browser.BrowserTabViewModel import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.AppLink -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject @@ -38,18 +35,15 @@ interface AppLinksLauncher { } @ContributesBinding(AppScope::class) -class DuckDuckGoAppLinksLauncher @Inject constructor( - private val appBuildConfig: AppBuildConfig, -) : AppLinksLauncher { +class DuckDuckGoAppLinksLauncher @Inject constructor() : AppLinksLauncher { - @Suppress("NewApi") override fun openAppLink(context: Context?, appLink: AppLink, viewModel: BrowserTabViewModel) { if (context == null) return appLink.appIntent?.let { it.flags = Intent.FLAG_ACTIVITY_NEW_TASK startActivityOrQuietlyFail(context, it) } ?: run { - if (appLink.excludedComponents != null && appBuildConfig.sdkInt >= Build.VERSION_CODES.N) { + if (appLink.excludedComponents != null) { val title = context.getString(R.string.appLinkIntentChooserTitle) val chooserIntent = getChooserIntent(appLink.uriString, title, appLink.excludedComponents!!) startActivityOrQuietlyFail(context, chooserIntent) @@ -58,7 +52,6 @@ class DuckDuckGoAppLinksLauncher @Inject constructor( viewModel.clearPreviousUrl() } - @RequiresApi(Build.VERSION_CODES.N) private fun getChooserIntent(url: String?, title: String, excludedComponents: List): Intent { val urlIntent = Intent.parseUri(url, Intent.URI_ANDROID_APP_SCHEME).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK From 1b53c44960e28ccc8d65af5cd5edee1327cf70e4 Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Thu, 22 Feb 2024 09:37:41 +0000 Subject: [PATCH 17/19] Disable no notifications prompt experiment (#4191) Task/Issue URL: https://app.asana.com/0/488551667048375/1206465832156620/f ### Description Disable experiment from local privacy_config.json ### Steps to test this PR _Android >= 13_ - [x] Fresh install from branch - [x] Check notifications permissions prompt appears - [x] Check onboarding works as expected _Android < 13_ - [x] Fresh install from branch - [x] Check onboarding works as expected _Existing sample users_ - [x] Remove DuckDuckGo folder from Downloads directory - [x] Fresh install from develop - [x] Make sure you are already part of the experiment (variants `mg` or `mf` already assigned) - [x] Install from branch - [x] Check you still have variant assigned ### No UI changes --- .../src/main/res/raw/privacy_config.json | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json b/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json index 623ec139ef31..f59a95cf6476 100644 --- a/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json +++ b/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json @@ -947,50 +947,7 @@ "reason": "Site breakage" } ] - }, - "notificationPermissions": { - "state": "enabled", - "features": { - "noPermissionsPrompt": { - "state": "enabled", - "targets": [ - { - "variantKey": "mg" - } - ] - } - } } }, - "unprotectedTemporary": [], - "experimentalVariants": { - "variants": [ - { - "desc": "this is SERP don't remove", - "variantKey": "sc", - "weight": 0.0 - }, - { - "desc": "this is SERP don't remove", - "variantKey": "se", - "weight": 0.0 - }, - { - "desc": "Control group for notification permissions experiment", - "variantKey": "mf", - "weight": 1.0, - "filters": { - "androidVersion": ["34", "33"] - } - }, - { - "desc": "No notifications permissions prompt experimental group", - "variantKey": "mg", - "weight": 1.0, - "filters": { - "androidVersion": ["34", "33"] - } - } - ] - } + "unprotectedTemporary": [] } \ No newline at end of file From 44760625d84b69e1870f39995a02e6b5bdd0e23e Mon Sep 17 00:00:00 2001 From: Lukasz Macionczyk Date: Thu, 22 Feb 2024 11:16:59 +0100 Subject: [PATCH 18/19] Pixels to measure subscription funnel (#4202) Task/Issue URL: https://app.asana.com/0/1205648422731273/1206637905023887/f ### Description See task. ### No UI changes --- subscriptions/subscriptions-impl/build.gradle | 1 + .../impl/SubscriptionsManager.kt | 35 +++- .../impl/billing/BillingClientWrapper.kt | 3 + .../SubscriptionMessagingInterface.kt | 7 +- .../impl/pixels/SubscriptionPixel.kt | 161 ++++++++++++++++ .../impl/pixels/SubscriptionPixelSender.kt | 182 ++++++++++++++++++ .../SubscriptionRefreshRetentionAtbPlugin.kt | 44 +++++ .../settings/views/ItrSettingViewModel.kt | 11 +- .../settings/views/PirSettingViewModel.kt | 11 +- .../impl/settings/views/ProSettingView.kt | 62 ++++++ .../settings/views/ProSettingViewModel.kt | 8 +- .../impl/ui/AddDeviceViewModel.kt | 4 + .../impl/ui/RestoreSubscriptionViewModel.kt | 16 +- .../impl/ui/SubscriptionSettingsActivity.kt | 12 ++ .../impl/ui/SubscriptionSettingsViewModel.kt | 4 + .../impl/ui/SubscriptionWebViewViewModel.kt | 13 ++ .../impl/ui/SubscriptionsWebViewActivity.kt | 8 + .../impl/RealSubscriptionsManagerTest.kt | 104 ++++++++++ .../SubscriptionMessagingInterfaceTest.kt | 7 + .../impl/pixels/SubscriptionPixelTest.kt | 47 +++++ ...bscriptionRefreshRetentionAtbPluginTest.kt | 44 +++++ .../settings/views/ItrSettingViewModelTest.kt | 11 +- .../settings/views/PirSettingViewModelTest.kt | 11 +- .../impl/ui/AddDeviceViewModelTest.kt | 11 +- .../ui/RestoreSubscriptionViewModelTest.kt | 65 ++++++- .../ui/SubscriptionSettingsViewModelTest.kt | 11 +- .../ui/SubscriptionWebViewViewModelTest.kt | 75 ++++++++ 27 files changed, 946 insertions(+), 22 deletions(-) create mode 100644 subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixel.kt create mode 100644 subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt create mode 100644 subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionRefreshRetentionAtbPlugin.kt create mode 100644 subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelTest.kt create mode 100644 subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionRefreshRetentionAtbPluginTest.kt diff --git a/subscriptions/subscriptions-impl/build.gradle b/subscriptions/subscriptions-impl/build.gradle index 900469244e2f..8e352d1aa5af 100644 --- a/subscriptions/subscriptions-impl/build.gradle +++ b/subscriptions/subscriptions-impl/build.gradle @@ -49,6 +49,7 @@ dependencies { implementation project(path: ':macos-api') implementation project(path: ':windows-api') implementation project(path: ':downloads-api') + implementation project(path: ':statistics') implementation AndroidX.appCompat implementation KotlinX.coroutines.core diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt index 467086fb0925..14e574bc45bf 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt @@ -32,6 +32,7 @@ import com.duckduckgo.subscriptions.impl.SubscriptionStatus.Unknown import com.duckduckgo.subscriptions.impl.SubscriptionsData.* import com.duckduckgo.subscriptions.impl.billing.BillingClientWrapper import com.duckduckgo.subscriptions.impl.billing.PurchaseState +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.repository.AuthRepository import com.duckduckgo.subscriptions.impl.services.AuthService import com.duckduckgo.subscriptions.impl.services.CreateAccountResponse @@ -40,6 +41,8 @@ import com.duckduckgo.subscriptions.impl.services.ResponseError import com.duckduckgo.subscriptions.impl.services.StoreLoginBody import com.duckduckgo.subscriptions.impl.services.SubscriptionsService import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.moshi.JsonDataException +import com.squareup.moshi.JsonEncodingException import com.squareup.moshi.Moshi import dagger.SingleInstanceIn import javax.inject.Inject @@ -142,6 +145,7 @@ class RealSubscriptionsManager @Inject constructor( private val context: Context, @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, + private val pixelSender: SubscriptionPixelSender, ) : SubscriptionsManager { private val adapter = Moshi.Builder().build().adapter(ResponseError::class.java) @@ -246,8 +250,11 @@ class RealSubscriptionsManager @Inject constructor( retries++ } if (hasSubscription) { + pixelSender.reportPurchaseSuccess() + pixelSender.reportSubscriptionActivated() _currentPurchaseState.emit(CurrentPurchase.Success) } else { + pixelSender.reportPurchaseFailureBackend() _currentPurchaseState.emit(CurrentPurchase.Failure("An error happened, try again")) } _hasSubscription.emit(hasSubscription) @@ -293,7 +300,11 @@ class RealSubscriptionsManager @Inject constructor( val storeLoginBody = StoreLoginBody(signature = signature, signedData = body, packageName = context.packageName) val response = authService.storeLogin(storeLoginBody) logcat(LogPriority.DEBUG) { "Subs: store login succeeded" } - authenticate(response.authToken) + val subscriptionsData = authenticate(response.authToken) + if (subscriptionsData is Success && subscriptionsData.entitlements.isNotEmpty()) { + pixelSender.reportSubscriptionActivated() + } + subscriptionsData } else { Failure(SUBSCRIPTION_NOT_FOUND_ERROR) } @@ -342,11 +353,13 @@ class RealSubscriptionsManager @Inject constructor( billingClientWrapper.launchBillingFlow(activity, billingParams) } } else { + pixelSender.reportRestoreAfterPurchaseAttemptSuccess() _currentPurchaseState.emit(CurrentPurchase.Recovered) } } is Failure -> { logcat(LogPriority.ERROR) { "Subs: ${response.message}" } + pixelSender.reportPurchaseFailureOther() _currentPurchaseState.emit(CurrentPurchase.Failure(response.message)) } } @@ -356,6 +369,11 @@ class RealSubscriptionsManager @Inject constructor( return try { val subscriptionData = if (isUserAuthenticated()) { getSubscriptionDataFromToken(authRepository.tokens().accessToken!!) + .also { subscriptionsData -> + if (subscriptionsData is Success && subscriptionsData.entitlements.isNotEmpty()) { + pixelSender.reportSubscriptionActivated() + } + } } else { recoverSubscriptionFromStore() } @@ -436,7 +454,20 @@ class RealSubscriptionsManager @Inject constructor( } private suspend fun createAccount(): CreateAccountResponse { - return authService.createAccount("Bearer ${emailManager.getToken()}") + try { + val account = authService.createAccount("Bearer ${emailManager.getToken()}") + if (account.authToken.isEmpty()) { + pixelSender.reportPurchaseFailureAccountCreation() + } + return account + } catch (e: Exception) { + when (e) { + is JsonDataException, is JsonEncodingException, is HttpException -> { + pixelSender.reportPurchaseFailureAccountCreation() + } + } + throw e + } } private fun parseError(e: HttpException): ResponseError? { diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/billing/BillingClientWrapper.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/billing/BillingClientWrapper.kt index 89ff99ac3afc..6ab0d22c4b8c 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/billing/BillingClientWrapper.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/billing/BillingClientWrapper.kt @@ -45,6 +45,7 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.LIST_OF_PRODUCTS import com.duckduckgo.subscriptions.impl.billing.PurchaseState.Canceled import com.duckduckgo.subscriptions.impl.billing.PurchaseState.InProgress import com.duckduckgo.subscriptions.impl.billing.PurchaseState.Purchased +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesMultibinding import dagger.SingleInstanceIn @@ -80,6 +81,7 @@ class RealBillingClientWrapper @Inject constructor( private val context: Context, val dispatcherProvider: DispatcherProvider, @AppCoroutineScope val coroutineScope: CoroutineScope, + private val pixelSender: SubscriptionPixelSender, ) : BillingClientWrapper, MainProcessLifecycleObserver { private var billingFlowInProcess = false @@ -109,6 +111,7 @@ class RealBillingClientWrapper @Inject constructor( } // Handle an error caused by a user cancelling the purchase flow. } else { + pixelSender.reportPurchaseFailureStore() coroutineScope.launch(dispatcherProvider.io()) { _purchaseState.emit(Canceled) } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt index 84df0d960def..5db335fb7a13 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt @@ -35,6 +35,7 @@ import com.duckduckgo.js.messaging.api.SubscriptionEventData import com.duckduckgo.subscriptions.impl.AuthToken import com.duckduckgo.subscriptions.impl.JSONObjectAdapter import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.squareup.anvil.annotations.ContributesBinding import com.squareup.moshi.Moshi import javax.inject.Inject @@ -52,6 +53,7 @@ class SubscriptionMessagingInterface @Inject constructor( private val jsMessageHelper: JsMessageHelper, private val dispatcherProvider: DispatcherProvider, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val pixelSender: SubscriptionPixelSender, ) : JsMessaging { private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build() @@ -61,7 +63,7 @@ class SubscriptionMessagingInterface @Inject constructor( private val handlers = listOf( SubscriptionsHandler(), GetSubscriptionMessage(subscriptionsManager, dispatcherProvider), - SetSubscriptionMessage(subscriptionsManager, appCoroutineScope, dispatcherProvider), + SetSubscriptionMessage(subscriptionsManager, appCoroutineScope, dispatcherProvider, pixelSender), ) @JavascriptInterface @@ -185,12 +187,15 @@ class SubscriptionMessagingInterface @Inject constructor( private val subscriptionsManager: SubscriptionsManager, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, + private val pixelSender: SubscriptionPixelSender, ) : JsMessageHandler { override fun process(jsMessage: JsMessage, secret: String, jsMessageCallback: JsMessageCallback?) { try { val token = jsMessage.params.getString("token") appCoroutineScope.launch(dispatcherProvider.io()) { subscriptionsManager.authenticate(token) + pixelSender.reportRestoreUsingEmailSuccess() + pixelSender.reportSubscriptionActivated() } } catch (e: Exception) { logcat { "Error parsing the token" } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixel.kt new file mode 100644 index 000000000000..2151b78162ed --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixel.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 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.subscriptions.impl.pixels + +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.COUNT +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.UNIQUE +import java.util.EnumSet + +enum class SubscriptionPixel( + private val baseName: String, + private val types: Set, +) { + SETTINGS_SUBSCRIPTION_SECTION_SHOWN( + baseName = "m_privacy-pro_app-settings_privacy-pro-section_impression", + type = COUNT, + ), + SUBSCRIPTION_ACTIVE( + baseName = "m_privacy-pro_app_subscription_active", + type = DAILY, + ), + OFFER_SCREEN_SHOWN( + baseName = "m_privacy-pro_offer_screen_impression", + type = COUNT, + ), + OFFER_SUBSCRIBE_CLICK( + baseName = "m_privacy-pro_terms-conditions_subscribe_click", + types = EnumSet.of(COUNT, DAILY), + ), + PURCHASE_FAILURE_OTHER( + baseName = "m_privacy-pro_app_subscription-purchase_failure_other", + types = EnumSet.of(COUNT, DAILY), + ), + PURCHASE_FAILURE_STORE( + baseName = "m_privacy-pro_app_subscription-purchase_failure_store", + types = EnumSet.of(COUNT, DAILY), + ), + PURCHASE_FAILURE_BACKEND( + baseName = "m_privacy-pro_app_subscription-purchase_failure_backend", + types = EnumSet.of(COUNT, DAILY), + ), + PURCHASE_FAILURE_ACCOUNT_CREATION( + baseName = "m_privacy-pro_app_subscription-purchase_failure_account-creation", + types = EnumSet.of(COUNT, DAILY), + ), + PURCHASE_SUCCESS( + baseName = "m_privacy-pro_app_subscription-purchase_success", + types = EnumSet.of(COUNT, DAILY), + ), + OFFER_RESTORE_PURCHASE_CLICK( + baseName = "m_privacy-pro_offer_restore-purchase_click", + type = COUNT, + ), + ACTIVATE_SUBSCRIPTION_ENTER_EMAIL_CLICK( + baseName = "m_privacy-pro_activate-subscription_enter-email_click", + types = EnumSet.of(COUNT, DAILY), + ), + ACTIVATE_SUBSCRIPTION_RESTORE_PURCHASE_CLICK( + baseName = "m_privacy-pro_activate-subscription_restore-purchase_click", + types = EnumSet.of(COUNT, DAILY), + ), + RESTORE_USING_EMAIL_SUCCESS( + baseName = "m_privacy-pro_app_subscription-restore-using-email_success", + types = EnumSet.of(COUNT, DAILY), + ), + RESTORE_USING_STORE_SUCCESS( + baseName = "m_privacy-pro_app_subscription-restore-using-store_success", + types = EnumSet.of(COUNT, DAILY), + ), + RESTORE_USING_STORE_FAILURE_SUBSCRIPTION_NOT_FOUND( + baseName = "m_privacy-pro_app_subscription-restore-using-store_failure_not-found", + types = EnumSet.of(COUNT, DAILY), + ), + RESTORE_USING_STORE_FAILURE_OTHER( + baseName = "m_privacy-pro_app_subscription-restore-using-store_failure_other", + types = EnumSet.of(COUNT, DAILY), + ), + RESTORE_AFTER_PURCHASE_ATTEMPT_SUCCESS( + baseName = "m_privacy-pro_app_subscription-restore-after-purchase-attempt_success", + type = COUNT, + ), + SUBSCRIPTION_ACTIVATED( + baseName = "m_privacy-pro_app_subscription_activated", + type = UNIQUE, + ), + ONBOARDING_ADD_DEVICE_CLICK( + baseName = "m_privacy-pro_welcome_add-device_click", + type = UNIQUE, + ), + SETTINGS_ADD_DEVICE_CLICK( + baseName = "m_privacy-pro_settings_add-device_click", + type = COUNT, + ), + ADD_DEVICE_ENTER_EMAIL_CLICK( + baseName = "m_privacy-pro_add-device_enter-email_click", + type = COUNT, + ), + ONBOARDING_VPN_CLICK( + baseName = "m_privacy-pro_welcome_vpn_click", + type = UNIQUE, + ), + ONBOARDING_PIR_CLICK( + baseName = "m_privacy-pro_welcome_personal-information-removal_click", + type = UNIQUE, + ), + ONBOARDING_IDTR_CLICK( + baseName = "m_privacy-pro_welcome_identity-theft-restoration_click", + type = UNIQUE, + ), + SUBSCRIPTION_SETTINGS_SHOWN( + baseName = "m_privacy-pro_settings_screen_impression", + type = COUNT, + ), + APP_SETTINGS_PIR_CLICK( + baseName = "m_privacy-pro_app-settings_personal-information-removal_click", + type = COUNT, + ), + APP_SETTINGS_IDTR_CLICK( + baseName = "m_privacy-pro_app-settings_identity-theft-restoration_click", + type = COUNT, + ), + SUBSCRIPTION_SETTINGS_CHANGE_PLAN_OR_BILLING_CLICK( + baseName = "m_privacy-pro_settings_change-plan-or-billing_click", + type = COUNT, + ), + SUBSCRIPTION_SETTINGS_REMOVE_FROM_DEVICE_CLICK( + baseName = "m_privacy-pro_settings_remove-from-device_click", + type = COUNT, + ), + ; + + constructor( + baseName: String, + type: PixelType, + ) : this(baseName, EnumSet.of(type)) + + fun getPixelNames(): Map = + types.associateWith { type -> "${baseName}_${type.pixelNameSuffix}" } +} + +private val PixelType.pixelNameSuffix: String + get() = when (this) { + COUNT -> "c" + DAILY -> "d" + UNIQUE -> "u" + } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt new file mode 100644 index 000000000000..ea11dc2e3eed --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2024 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.subscriptions.impl.pixels + +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ACTIVATE_SUBSCRIPTION_ENTER_EMAIL_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ACTIVATE_SUBSCRIPTION_RESTORE_PURCHASE_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ADD_DEVICE_ENTER_EMAIL_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.APP_SETTINGS_IDTR_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.APP_SETTINGS_PIR_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_RESTORE_PURCHASE_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SCREEN_SHOWN +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SUBSCRIBE_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_ADD_DEVICE_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_IDTR_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_PIR_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_VPN_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.PURCHASE_FAILURE_ACCOUNT_CREATION +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.PURCHASE_FAILURE_BACKEND +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.PURCHASE_FAILURE_OTHER +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.PURCHASE_FAILURE_STORE +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.PURCHASE_SUCCESS +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.RESTORE_AFTER_PURCHASE_ATTEMPT_SUCCESS +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.RESTORE_USING_EMAIL_SUCCESS +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.RESTORE_USING_STORE_FAILURE_OTHER +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.RESTORE_USING_STORE_FAILURE_SUBSCRIPTION_NOT_FOUND +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.RESTORE_USING_STORE_SUCCESS +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.SETTINGS_ADD_DEVICE_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.SETTINGS_SUBSCRIPTION_SECTION_SHOWN +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.SUBSCRIPTION_ACTIVATED +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.SUBSCRIPTION_ACTIVE +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.SUBSCRIPTION_SETTINGS_CHANGE_PLAN_OR_BILLING_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.SUBSCRIPTION_SETTINGS_REMOVE_FROM_DEVICE_CLICK +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.SUBSCRIPTION_SETTINGS_SHOWN +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject + +interface SubscriptionPixelSender { + fun reportSubscriptionSettingsSectionShown() + fun reportSubscriptionActive() + fun reportOfferScreenShown() + fun reportOfferSubscribeClick() + fun reportPurchaseFailureOther() + fun reportPurchaseFailureStore() + fun reportPurchaseFailureBackend() + fun reportPurchaseFailureAccountCreation() + fun reportPurchaseSuccess() + fun reportOfferRestorePurchaseClick() + fun reportActivateSubscriptionEnterEmailClick() + fun reportActivateSubscriptionRestorePurchaseClick() + fun reportRestoreUsingEmailSuccess() + fun reportRestoreUsingStoreSuccess() + fun reportRestoreUsingStoreFailureSubscriptionNotFound() + fun reportRestoreUsingStoreFailureOther() + fun reportRestoreAfterPurchaseAttemptSuccess() + fun reportSubscriptionActivated() + fun reportOnboardingAddDeviceClick() + fun reportSettingsAddDeviceClick() + fun reportAddDeviceEnterEmailClick() + fun reportOnboardingVpnClick() + fun reportOnboardingPirClick() + fun reportOnboardingIdtrClick() + fun reportSubscriptionSettingsShown() + fun reportAppSettingsPirClick() + fun reportAppSettingsIdtrClick() + fun reportSubscriptionSettingsChangePlanOrBillingClick() + fun reportSubscriptionSettingsRemoveFromDeviceClick() +} + +@ContributesBinding(AppScope::class) +class SubscriptionPixelSenderImpl @Inject constructor( + private val pixelSender: Pixel, +) : SubscriptionPixelSender { + + override fun reportSubscriptionSettingsSectionShown() = + fire(SETTINGS_SUBSCRIPTION_SECTION_SHOWN) + + override fun reportSubscriptionActive() = + fire(SUBSCRIPTION_ACTIVE) + + override fun reportOfferScreenShown() = + fire(OFFER_SCREEN_SHOWN) + + override fun reportOfferSubscribeClick() = + fire(OFFER_SUBSCRIBE_CLICK) + + override fun reportPurchaseFailureOther() = + fire(PURCHASE_FAILURE_OTHER) + + override fun reportPurchaseFailureStore() = + fire(PURCHASE_FAILURE_STORE) + + override fun reportPurchaseFailureBackend() = + fire(PURCHASE_FAILURE_BACKEND) + + override fun reportPurchaseFailureAccountCreation() = + fire(PURCHASE_FAILURE_ACCOUNT_CREATION) + + override fun reportPurchaseSuccess() = + fire(PURCHASE_SUCCESS) + + override fun reportOfferRestorePurchaseClick() = + fire(OFFER_RESTORE_PURCHASE_CLICK) + + override fun reportActivateSubscriptionEnterEmailClick() = + fire(ACTIVATE_SUBSCRIPTION_ENTER_EMAIL_CLICK) + + override fun reportActivateSubscriptionRestorePurchaseClick() = + fire(ACTIVATE_SUBSCRIPTION_RESTORE_PURCHASE_CLICK) + + override fun reportRestoreUsingEmailSuccess() = + fire(RESTORE_USING_EMAIL_SUCCESS) + + override fun reportRestoreUsingStoreSuccess() = + fire(RESTORE_USING_STORE_SUCCESS) + + override fun reportRestoreUsingStoreFailureSubscriptionNotFound() = + fire(RESTORE_USING_STORE_FAILURE_SUBSCRIPTION_NOT_FOUND) + + override fun reportRestoreUsingStoreFailureOther() = + fire(RESTORE_USING_STORE_FAILURE_OTHER) + + override fun reportRestoreAfterPurchaseAttemptSuccess() = + fire(RESTORE_AFTER_PURCHASE_ATTEMPT_SUCCESS) + + override fun reportSubscriptionActivated() = + fire(SUBSCRIPTION_ACTIVATED) + + override fun reportOnboardingAddDeviceClick() = + fire(ONBOARDING_ADD_DEVICE_CLICK) + + override fun reportSettingsAddDeviceClick() = + fire(SETTINGS_ADD_DEVICE_CLICK) + + override fun reportAddDeviceEnterEmailClick() = + fire(ADD_DEVICE_ENTER_EMAIL_CLICK) + + override fun reportOnboardingVpnClick() = + fire(ONBOARDING_VPN_CLICK) + + override fun reportOnboardingPirClick() = + fire(ONBOARDING_PIR_CLICK) + + override fun reportOnboardingIdtrClick() = + fire(ONBOARDING_IDTR_CLICK) + + override fun reportSubscriptionSettingsShown() = + fire(SUBSCRIPTION_SETTINGS_SHOWN) + + override fun reportAppSettingsPirClick() = + fire(APP_SETTINGS_PIR_CLICK) + + override fun reportAppSettingsIdtrClick() = + fire(APP_SETTINGS_IDTR_CLICK) + + override fun reportSubscriptionSettingsChangePlanOrBillingClick() = + fire(SUBSCRIPTION_SETTINGS_CHANGE_PLAN_OR_BILLING_CLICK) + + override fun reportSubscriptionSettingsRemoveFromDeviceClick() = + fire(SUBSCRIPTION_SETTINGS_REMOVE_FROM_DEVICE_CLICK) + + private fun fire(pixel: SubscriptionPixel) { + pixel.getPixelNames().forEach { (pixelType, pixelName) -> + pixelSender.fire(pixelName = pixelName, type = pixelType) + } + } +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionRefreshRetentionAtbPlugin.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionRefreshRetentionAtbPlugin.kt new file mode 100644 index 000000000000..978fb5f4f99b --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionRefreshRetentionAtbPlugin.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 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.subscriptions.impl.pixels + +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.statistics.api.RefreshRetentionAtbPlugin +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@ContributesMultibinding(AppScope::class) +class SubscriptionRefreshRetentionAtbPlugin @Inject constructor( + @AppCoroutineScope private val coroutineScope: CoroutineScope, + private val subscriptionsManager: SubscriptionsManager, + private val pixelSender: SubscriptionPixelSender, +) : RefreshRetentionAtbPlugin { + + override fun onSearchRetentionAtbRefreshed() = Unit // no-op + + override fun onAppRetentionAtbRefreshed() { + coroutineScope.launch { + if (subscriptionsManager.hasSubscription()) { + pixelSender.reportSubscriptionActive() + } + } + } +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt index 86070893f4ee..0788b9c872bf 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt @@ -27,8 +27,10 @@ import com.duckduckgo.subscriptions.api.Product.ITR import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.subscriptions.api.Subscriptions.EntitlementStatus.Found import com.duckduckgo.subscriptions.api.Subscriptions.EntitlementStatus.NotFound +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.Command.OpenItr import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -38,9 +40,10 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle -class ItrSettingViewModel( +class ItrSettingViewModel @Inject constructor( private val subscriptions: Subscriptions, private val dispatcherProvider: DispatcherProvider, + private val pixelSender: SubscriptionPixelSender, ) : ViewModel(), DefaultLifecycleObserver { sealed class Command { @@ -55,6 +58,7 @@ class ItrSettingViewModel( val viewState = _viewState.asStateFlow() fun onItr() { + pixelSender.reportAppSettingsIdtrClick() sendCommand(OpenItr) } @@ -77,13 +81,12 @@ class ItrSettingViewModel( @Suppress("UNCHECKED_CAST") class Factory @Inject constructor( - private val subscriptions: Subscriptions, - private val dispatcherProvider: DispatcherProvider, + private val itrSettingViewModel: Provider, ) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class): T { return with(modelClass) { when { - isAssignableFrom(ItrSettingViewModel::class.java) -> ItrSettingViewModel(subscriptions, dispatcherProvider) + isAssignableFrom(ItrSettingViewModel::class.java) -> itrSettingViewModel.get() else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } } as T diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt index e86678d761f9..52c6f88def75 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt @@ -27,8 +27,10 @@ import com.duckduckgo.subscriptions.api.Product.PIR import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.subscriptions.api.Subscriptions.EntitlementStatus.Found import com.duckduckgo.subscriptions.api.Subscriptions.EntitlementStatus.NotFound +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.Command.OpenPir import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -38,9 +40,10 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle -class PirSettingViewModel( +class PirSettingViewModel @Inject constructor( private val subscriptions: Subscriptions, private val dispatcherProvider: DispatcherProvider, + private val pixelSender: SubscriptionPixelSender, ) : ViewModel(), DefaultLifecycleObserver { sealed class Command { @@ -55,6 +58,7 @@ class PirSettingViewModel( val viewState = _viewState.asStateFlow() fun onPir() { + pixelSender.reportAppSettingsPirClick() sendCommand(OpenPir) } @@ -77,13 +81,12 @@ class PirSettingViewModel( @Suppress("UNCHECKED_CAST") class Factory @Inject constructor( - private val subscriptions: Subscriptions, - private val dispatcherProvider: DispatcherProvider, + private val pirSettingViewModel: Provider, ) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class): T { return with(modelClass) { when { - isAssignableFrom(PirSettingViewModel::class.java) -> PirSettingViewModel(subscriptions, dispatcherProvider) + isAssignableFrom(PirSettingViewModel::class.java) -> pirSettingViewModel.get() else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } } as T diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt index 3d15bbddac28..032ebdde02b9 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt @@ -18,10 +18,16 @@ package com.duckduckgo.subscriptions.impl.settings.views import android.annotation.SuppressLint import android.content.Context +import android.graphics.Rect import android.util.AttributeSet import android.view.MotionEvent +import android.view.View +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.view.ViewTreeObserver.OnScrollChangedListener import android.widget.FrameLayout import android.widget.LinearLayout +import androidx.core.view.doOnAttach +import androidx.core.view.doOnDetach import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner @@ -36,6 +42,7 @@ import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.subscriptions.impl.R import com.duckduckgo.subscriptions.impl.SubscriptionsConstants import com.duckduckgo.subscriptions.impl.databinding.ViewSettingsBinding +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenBuyScreen import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenSettings @@ -65,6 +72,9 @@ class ProSettingView @JvmOverloads constructor( @Inject lateinit var globalActivityStarter: GlobalActivityStarter + @Inject + lateinit var pixelSender: SubscriptionPixelSender + private var coroutineScope: CoroutineScope? = null private val binding: ViewSettingsBinding by viewBinding() @@ -91,6 +101,10 @@ class ProSettingView @JvmOverloads constructor( viewModel.viewState .onEach { renderView(it) } .launchIn(coroutineScope!!) + + binding.subscribeSecondary.doOnFullyVisible { + pixelSender.reportSubscriptionSettingsSectionShown() + } } override fun onDetachedFromWindow() { @@ -155,3 +169,51 @@ class SubscriptionSettingLayout @JvmOverloads constructor( return true } } + +private fun View.doOnFullyVisible(action: () -> Unit) { + val listener = object : OnGlobalLayoutListener, OnScrollChangedListener { + var actionInvoked = false + + override fun onGlobalLayout() { + onPotentialVisibilityChange() + } + + override fun onScrollChanged() { + onPotentialVisibilityChange() + } + + fun onPotentialVisibilityChange() { + if (!actionInvoked && isViewFullyVisible()) { + actionInvoked = true + action() + } + + if (actionInvoked) { + unregister() + } + } + + fun isViewFullyVisible(): Boolean { + val visibleRect = Rect() + val isGlobalVisible = getGlobalVisibleRect(visibleRect) + return isGlobalVisible && width == visibleRect.width() && height == visibleRect.height() + } + + fun register() { + viewTreeObserver.addOnGlobalLayoutListener(this) + viewTreeObserver.addOnScrollChangedListener(this) + } + + fun unregister() { + viewTreeObserver.removeOnGlobalLayoutListener(this) + viewTreeObserver.removeOnScrollChangedListener(this) + } + } + + doOnAttach { + listener.register() + doOnDetach { + listener.unregister() + } + } +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt index b147c551ba85..92db577068c0 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt @@ -27,6 +27,7 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsManager import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenBuyScreen import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenSettings import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -36,7 +37,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle -class ProSettingViewModel( +class ProSettingViewModel @Inject constructor( private val subscriptionsManager: SubscriptionsManager, private val dispatcherProvider: DispatcherProvider, ) : ViewModel(), DefaultLifecycleObserver { @@ -78,13 +79,12 @@ class ProSettingViewModel( @Suppress("UNCHECKED_CAST") class Factory @Inject constructor( - private val subscriptionsManager: SubscriptionsManager, - private val dispatcherProvider: DispatcherProvider, + private val proSettingViewModel: Provider, ) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class): T { return with(modelClass) { when { - isAssignableFrom(ProSettingViewModel::class.java) -> ProSettingViewModel(subscriptionsManager, dispatcherProvider) + isAssignableFrom(ProSettingViewModel::class.java) -> proSettingViewModel.get() else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } } as T diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/AddDeviceViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/AddDeviceViewModel.kt index 30919a259157..de2abea2dce5 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/AddDeviceViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/AddDeviceViewModel.kt @@ -26,6 +26,7 @@ import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.subscriptions.impl.SubscriptionsData import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.ui.AddDeviceViewModel.Command.AddEmail import com.duckduckgo.subscriptions.impl.ui.AddDeviceViewModel.Command.Error import com.duckduckgo.subscriptions.impl.ui.AddDeviceViewModel.Command.ManageEmail @@ -43,6 +44,7 @@ import kotlinx.coroutines.launch class AddDeviceViewModel @Inject constructor( private val subscriptionsManager: SubscriptionsManager, private val dispatcherProvider: DispatcherProvider, + private val pixelSender: SubscriptionPixelSender, ) : ViewModel(), DefaultLifecycleObserver { private val command = Channel(1, DROP_OLDEST) @@ -69,6 +71,8 @@ class AddDeviceViewModel @Inject constructor( } fun useEmail() { + pixelSender.reportAddDeviceEnterEmailClick() + viewModelScope.launch(dispatcherProvider.io()) { val subs = subscriptionsManager.getSubscriptionData() if (subs is SubscriptionsData.Success) { diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModel.kt index 98c3f1bd25f9..2049dcda3904 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModel.kt @@ -24,6 +24,7 @@ import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.subscriptions.impl.RealSubscriptionsManager.Companion.SUBSCRIPTION_NOT_FOUND_ERROR import com.duckduckgo.subscriptions.impl.SubscriptionsData import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.ui.RestoreSubscriptionViewModel.Command.Error import com.duckduckgo.subscriptions.impl.ui.RestoreSubscriptionViewModel.Command.RestoreFromEmail import com.duckduckgo.subscriptions.impl.ui.RestoreSubscriptionViewModel.Command.SubscriptionNotFound @@ -41,6 +42,7 @@ import kotlinx.coroutines.launch class RestoreSubscriptionViewModel @Inject constructor( private val subscriptionsManager: SubscriptionsManager, private val dispatcherProvider: DispatcherProvider, + private val pixelSender: SubscriptionPixelSender, ) : ViewModel() { private val command = Channel(1, DROP_OLDEST) @@ -53,20 +55,29 @@ class RestoreSubscriptionViewModel @Inject constructor( ) fun restoreFromStore() { + pixelSender.reportActivateSubscriptionRestorePurchaseClick() viewModelScope.launch(dispatcherProvider.io()) { when (val response = subscriptionsManager.recoverSubscriptionFromStore()) { is SubscriptionsData.Success -> { if (response.entitlements.isEmpty()) { + pixelSender.reportRestoreUsingStoreFailureSubscriptionNotFound() subscriptionsManager.signOut() command.send(SubscriptionNotFound) } else { + pixelSender.reportRestoreUsingStoreSuccess() command.send(Success) } } is SubscriptionsData.Failure -> { when (response.message) { - SUBSCRIPTION_NOT_FOUND_ERROR -> command.send(SubscriptionNotFound) - else -> command.send(Error(response.message)) + SUBSCRIPTION_NOT_FOUND_ERROR -> { + pixelSender.reportRestoreUsingStoreFailureSubscriptionNotFound() + command.send(SubscriptionNotFound) + } + else -> { + pixelSender.reportRestoreUsingStoreFailureOther() + command.send(Error(response.message)) + } } } } @@ -74,6 +85,7 @@ class RestoreSubscriptionViewModel @Inject constructor( } fun restoreFromEmail() { + pixelSender.reportActivateSubscriptionEnterEmailClick() viewModelScope.launch { command.send(RestoreFromEmail) } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt index 0573004b38df..4428b3e6c877 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt @@ -34,6 +34,7 @@ import com.duckduckgo.subscriptions.impl.R.string import com.duckduckgo.subscriptions.impl.SubscriptionStatus.AutoRenewable import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.BASIC_SUBSCRIPTION import com.duckduckgo.subscriptions.impl.databinding.ActivitySubscriptionSettingsBinding +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.ui.AddDeviceActivity.Companion.AddDeviceScreenWithEmptyParams import com.duckduckgo.subscriptions.impl.ui.ChangePlanActivity.Companion.ChangePlanScreenWithEmptyParams import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsActivity.Companion.SubscriptionsSettingsScreenWithEmptyParams @@ -53,6 +54,9 @@ class SubscriptionSettingsActivity : DuckDuckGoActivity() { @Inject lateinit var globalActivityStarter: GlobalActivityStarter + @Inject + lateinit var pixelSender: SubscriptionPixelSender + private val viewModel: SubscriptionSettingsViewModel by bindViewModel() private val binding: ActivitySubscriptionSettingsBinding by viewBinding() @@ -76,6 +80,7 @@ class SubscriptionSettingsActivity : DuckDuckGoActivity() { }.launchIn(lifecycleScope) binding.addDevice.setClickListener { + pixelSender.reportSettingsAddDeviceClick() globalActivityStarter.start(this, AddDeviceScreenWithEmptyParams) } @@ -103,6 +108,10 @@ class SubscriptionSettingsActivity : DuckDuckGoActivity() { binding.faq.setClickListener { Toast.makeText(this, "This will take you to FAQs", Toast.LENGTH_SHORT).show() } + + if (savedInstanceState == null) { + pixelSender.reportSubscriptionSettingsShown() + } } override fun onDestroy() { @@ -121,15 +130,18 @@ class SubscriptionSettingsActivity : DuckDuckGoActivity() { when (viewState.platform?.lowercase()) { "apple", "ios" -> binding.changePlan.setClickListener { + pixelSender.reportSubscriptionSettingsChangePlanOrBillingClick() globalActivityStarter.start(this, ChangePlanScreenWithEmptyParams) } "stripe" -> { binding.changePlan.setClickListener { + pixelSender.reportSubscriptionSettingsChangePlanOrBillingClick() viewModel.goToStripe() } } else -> { binding.changePlan.setClickListener { + pixelSender.reportSubscriptionSettingsChangePlanOrBillingClick() val url = String.format(URL, BASIC_SUBSCRIPTION, applicationContext.packageName) val intent = Intent(Intent.ACTION_VIEW) intent.setData(Uri.parse(url)) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt index e7b6cfffbfd5..d210e03bf242 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt @@ -28,6 +28,7 @@ import com.duckduckgo.subscriptions.impl.Subscription import com.duckduckgo.subscriptions.impl.SubscriptionStatus import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.FinishSignOut import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.GoToPortal import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.SubscriptionDuration.Monthly @@ -48,6 +49,7 @@ import kotlinx.coroutines.launch class SubscriptionSettingsViewModel @Inject constructor( private val subscriptionsManager: SubscriptionsManager, private val dispatcherProvider: DispatcherProvider, + private val pixelSender: SubscriptionPixelSender, ) : ViewModel(), DefaultLifecycleObserver { private val command = Channel(1, DROP_OLDEST) @@ -82,6 +84,8 @@ class SubscriptionSettingsViewModel @Inject constructor( } fun removeFromDevice() { + pixelSender.reportSubscriptionSettingsRemoveFromDeviceClick() + viewModelScope.launch { subscriptionsManager.signOut() command.send(FinishSignOut) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt index c5b7358af625..db4879d34859 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt @@ -38,6 +38,7 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN import com.duckduckgo.subscriptions.impl.SubscriptionsManager import com.duckduckgo.subscriptions.impl.billing.getPrice +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.repository.SubscriptionsRepository import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.* import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.PurchaseStateView.Failure @@ -66,6 +67,7 @@ class SubscriptionWebViewViewModel @Inject constructor( private val subscriptionsManager: SubscriptionsManager, private val subscriptionsRepository: SubscriptionsRepository, private val networkProtectionWaitlist: NetworkProtectionWaitlist, + private val pixelSender: SubscriptionPixelSender, ) : ViewModel() { private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build() @@ -121,6 +123,12 @@ class SubscriptionWebViewViewModel @Inject constructor( PIR -> GoToPIR else -> null } + when (commandToSend) { + GoToITR -> pixelSender.reportOnboardingIdtrClick() + is GoToNetP -> pixelSender.reportOnboardingVpnClick() + GoToPIR -> pixelSender.reportOnboardingPirClick() + else -> {} // no-op + } commandToSend?.let { command.send(commandToSend) } @@ -129,17 +137,22 @@ class SubscriptionWebViewViewModel @Inject constructor( private fun activateSubscription() { viewModelScope.launch(dispatcherProvider.io()) { if (subscriptionsManager.hasSubscription()) { + pixelSender.reportOnboardingAddDeviceClick() activateOnAnotherDevice() } else { + pixelSender.reportOfferRestorePurchaseClick() recoverSubscription() } } } private fun subscriptionSelected(data: JSONObject?) { + pixelSender.reportOfferSubscribeClick() + viewModelScope.launch(dispatcherProvider.io()) { val id = runCatching { data?.getString("id") }.getOrNull() if (id.isNullOrBlank()) { + pixelSender.reportPurchaseFailureOther() _currentPurchaseViewState.emit(currentPurchaseViewState.value.copy(purchaseState = Failure(""))) } else { command.send(SubscriptionSelected(id)) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewActivity.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewActivity.kt index 2b732f217319..122cd6a9b90f 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewActivity.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewActivity.kt @@ -75,6 +75,7 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.ACTIVATE_URL import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.BUY_URL import com.duckduckgo.subscriptions.impl.databinding.ActivitySubscriptionsWebviewBinding import com.duckduckgo.subscriptions.impl.pir.PirActivity.Companion.PirScreenWithEmptyParams +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.ui.AddDeviceActivity.Companion.AddDeviceScreenWithEmptyParams import com.duckduckgo.subscriptions.impl.ui.RestoreSubscriptionActivity.Companion.RestoreSubscriptionScreenWithEmptyParams import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command @@ -145,6 +146,9 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD @Inject lateinit var downloadsFileActions: DownloadsFileActions + @Inject + lateinit var pixelSender: SubscriptionPixelSender + private val viewModel: SubscriptionWebViewViewModel by bindViewModel() private val binding: ActivitySubscriptionsWebviewBinding by viewBinding() @@ -245,6 +249,10 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD viewModel.currentPurchaseViewState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).distinctUntilChanged().onEach { renderPurchaseState(it.purchaseState) }.launchIn(lifecycleScope) + + if (savedInstanceState == null && url == BUY_URL) { + pixelSender.reportOfferScreenShown() + } } override fun continueDownload(pendingFileDownload: PendingFileDownload) { diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt index 999a83800cc9..37925221518f 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt @@ -18,6 +18,7 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsData.Failure import com.duckduckgo.subscriptions.impl.SubscriptionsData.Success import com.duckduckgo.subscriptions.impl.billing.BillingClientWrapper import com.duckduckgo.subscriptions.impl.billing.PurchaseState +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.repository.AuthRepository import com.duckduckgo.subscriptions.impl.repository.FakeAuthDataStore import com.duckduckgo.subscriptions.impl.repository.RealAuthRepository @@ -33,6 +34,7 @@ import com.duckduckgo.subscriptions.impl.services.SubscriptionsService import com.duckduckgo.subscriptions.impl.services.ValidateTokenResponse import com.duckduckgo.subscriptions.store.AuthDataStore import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -47,6 +49,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import retrofit2.HttpException import retrofit2.Response @@ -65,6 +68,7 @@ class RealSubscriptionsManagerTest { private val billingClient: BillingClientWrapper = mock() private val billingBuilder: BillingFlowParams.Builder = mock() private val context: Context = mock() + private val pixelSender: SubscriptionPixelSender = mock() private lateinit var subscriptionsManager: SubscriptionsManager @Before @@ -82,6 +86,7 @@ class RealSubscriptionsManagerTest { context, TestScope(), coroutineRule.testDispatcherProvider, + pixelSender, ) } @@ -416,6 +421,7 @@ class RealSubscriptionsManagerTest { context, TestScope(), coroutineRule.testDispatcherProvider, + pixelSender, ) manager.hasSubscription.test { @@ -438,6 +444,7 @@ class RealSubscriptionsManagerTest { context, TestScope(), coroutineRule.testDispatcherProvider, + pixelSender, ) manager.hasSubscription.test { @@ -460,6 +467,7 @@ class RealSubscriptionsManagerTest { context, TestScope(), coroutineRule.testDispatcherProvider, + pixelSender, ) manager.hasSubscription.test { @@ -481,6 +489,7 @@ class RealSubscriptionsManagerTest { context, TestScope(), coroutineRule.testDispatcherProvider, + pixelSender, ) manager.hasSubscription.test { @@ -502,6 +511,7 @@ class RealSubscriptionsManagerTest { context, TestScope(), coroutineRule.testDispatcherProvider, + pixelSender, ) manager.hasSubscription.test { @@ -527,6 +537,7 @@ class RealSubscriptionsManagerTest { context, TestScope(), coroutineRule.testDispatcherProvider, + pixelSender, ) manager.currentPurchaseState.test { @@ -554,6 +565,7 @@ class RealSubscriptionsManagerTest { context, TestScope(), coroutineRule.testDispatcherProvider, + pixelSender, ) manager.currentPurchaseState.test { @@ -753,6 +765,7 @@ class RealSubscriptionsManagerTest { context, TestScope(), coroutineRule.testDispatcherProvider, + pixelSender, ) manager.signOut() verify(mockRepo).signOut() @@ -785,6 +798,7 @@ class RealSubscriptionsManagerTest { context, TestScope(), coroutineRule.testDispatcherProvider, + pixelSender, ) manager.hasSubscription.test { @@ -795,6 +809,96 @@ class RealSubscriptionsManagerTest { } } + @Test + fun whenPurchaseIsSuccessfulThenPixelIsSent() = runTest { + givenUserIsAuthenticated() + givenValidateTokenSucceedsWithEntitlements() + + whenever(billingClient.purchaseState).thenReturn(flowOf(PurchaseState.Purchased)) + + subscriptionsManager.currentPurchaseState.test { + assertTrue(awaitItem() is CurrentPurchase.InProgress) + assertTrue(awaitItem() is CurrentPurchase.Success) + + verify(pixelSender).reportPurchaseSuccess() + verify(pixelSender).reportSubscriptionActivated() + verifyNoMoreInteractions(pixelSender) + + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun whenSubscriptionIsRestoredOnPurchaseAttemptThenPixelIsSent() = runTest { + givenUserIsNotAuthenticated() + givenPurchaseStored() + givenPurchaseStoredIsValid() + givenValidateTokenSucceedsWithEntitlements() + givenAuthenticateSucceeds() + + subscriptionsManager.currentPurchaseState.test { + subscriptionsManager.purchase(mock(), mock(), "", false) + assertTrue(awaitItem() is CurrentPurchase.PreFlowInProgress) + assertTrue(awaitItem() is CurrentPurchase.Recovered) + + verify(pixelSender).reportRestoreAfterPurchaseAttemptSuccess() + verify(pixelSender).reportSubscriptionActivated() + verifyNoMoreInteractions(pixelSender) + + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun whenPurchaseFailsThenPixelIsSent() = runTest { + givenUserIsAuthenticated() + givenValidateTokenFails("failure") + + whenever(billingClient.purchaseState).thenReturn(flowOf(PurchaseState.Purchased)) + + subscriptionsManager.currentPurchaseState.test { + assertTrue(awaitItem() is CurrentPurchase.InProgress) + assertTrue(awaitItem() is CurrentPurchase.Failure) + + verify(pixelSender).reportPurchaseFailureBackend() + verifyNoMoreInteractions(pixelSender) + + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun whenSubscriptionIsRecoveredFromStoreThenPixelIsSent() = runTest { + givenPurchaseStored() + givenPurchaseStoredIsValid() + givenValidateTokenSucceedsWithEntitlements() + givenAuthenticateSucceeds() + + val value = subscriptionsManager.recoverSubscriptionFromStore() + + assertTrue(value is Success) + verify(pixelSender).reportSubscriptionActivated() + verifyNoMoreInteractions(pixelSender) + } + + @Test + fun whenPurchaseFlowIfCreateAccountFailsThenPixelIsSent() = runTest { + givenUserIsNotAuthenticated() + givenCreateAccountFails() + + subscriptionsManager.currentPurchaseState.test { + subscriptionsManager.purchase(mock(), mock(), "", false) + assertTrue(awaitItem() is CurrentPurchase.PreFlowInProgress) + assertTrue(awaitItem() is CurrentPurchase.Failure) + + verify(pixelSender).reportPurchaseFailureAccountCreation() + verify(pixelSender).reportPurchaseFailureOther() + verifyNoMoreInteractions(pixelSender) + + cancelAndConsumeRemainingEvents() + } + } + private suspend fun givenUrlPortalSucceeds() { whenever(subscriptionsService.portal(any())).thenReturn(PortalResponse("example.com")) } diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterfaceTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterfaceTest.kt index 56df478c39ba..2f07797d0f2a 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterfaceTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterfaceTest.kt @@ -8,6 +8,7 @@ import com.duckduckgo.js.messaging.api.JsMessageHelper import com.duckduckgo.js.messaging.api.JsRequestResponse import com.duckduckgo.subscriptions.impl.AuthToken import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.json.JSONObject @@ -31,11 +32,13 @@ class SubscriptionMessagingInterfaceTest { private val webView: WebView = mock() private val jsMessageHelper: JsMessageHelper = mock() private val subscriptionsManager: SubscriptionsManager = mock() + private val pixelSender: SubscriptionPixelSender = mock() private val messagingInterface = SubscriptionMessagingInterface( subscriptionsManager, jsMessageHelper, coroutineRule.testDispatcherProvider, TestScope(), + pixelSender, ) private val callback = object : JsMessageCallback() { @@ -233,6 +236,7 @@ class SubscriptionMessagingInterfaceTest { messagingInterface.process(message, "duckduckgo-android-messaging-secret") verifyNoInteractions(subscriptionsManager) + verifyNoInteractions(pixelSender) } @Test @@ -246,6 +250,8 @@ class SubscriptionMessagingInterfaceTest { messagingInterface.process(message, "duckduckgo-android-messaging-secret") verify(subscriptionsManager).authenticate("authToken") + verify(pixelSender).reportRestoreUsingEmailSuccess() + verify(pixelSender).reportSubscriptionActivated() } @Test @@ -259,6 +265,7 @@ class SubscriptionMessagingInterfaceTest { messagingInterface.process(message, "duckduckgo-android-messaging-secret") verifyNoInteractions(subscriptionsManager) + verifyNoInteractions(pixelSender) } @Test diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelTest.kt new file mode 100644 index 000000000000..ddd365d6ae24 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelTest.kt @@ -0,0 +1,47 @@ +package com.duckduckgo.subscriptions.impl.pixels + +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.COUNT +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.UNIQUE +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class SubscriptionPixelTest( + private val pixel: SubscriptionPixel, +) { + @Test + fun `pixel name has privacy pro namespace prefix`() { + pixel.getPixelNames().values.forEach { pixelName -> + assertTrue(pixelName.startsWith("m_privacy-pro_")) + } + } + + @Test + fun `pixel name has pixel type suffix`() { + pixel.getPixelNames().forEach { (pixelType, pixelName) -> + val expectedSuffix = when (pixelType) { + COUNT -> "_c" + DAILY -> "_d" + UNIQUE -> "_u" + } + + assertTrue(pixelName.endsWith(expectedSuffix)) + } + } + + @Test + fun `pixel names map is not empty`() { + assertTrue(pixel.getPixelNames().isNotEmpty()) + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun data(): Collection> = + SubscriptionPixel.entries.map { arrayOf(it) } + } +} diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionRefreshRetentionAtbPluginTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionRefreshRetentionAtbPluginTest.kt new file mode 100644 index 000000000000..86cd001528e3 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionRefreshRetentionAtbPluginTest.kt @@ -0,0 +1,44 @@ +package com.duckduckgo.subscriptions.impl.pixels + +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class SubscriptionRefreshRetentionAtbPluginTest { + + @get:Rule + var coroutineRule = CoroutineTestRule() + + private val subscriptionsManager: SubscriptionsManager = mock() + private val pixelSender: SubscriptionPixelSender = mock() + + private val subject = SubscriptionRefreshRetentionAtbPlugin( + coroutineScope = coroutineRule.testScope, + subscriptionsManager = subscriptionsManager, + pixelSender = pixelSender, + ) + + @Test + fun `when subscription is active then pixel is sent`() = runTest { + whenever(subscriptionsManager.hasSubscription()).thenReturn(true) + + subject.onAppRetentionAtbRefreshed() + + verify(pixelSender).reportSubscriptionActive() + } + + @Test + fun `when subscription is not active then pixel is not sent`() = runTest { + whenever(subscriptionsManager.hasSubscription()).thenReturn(false) + + subject.onAppRetentionAtbRefreshed() + + verify(pixelSender, never()).reportSubscriptionActive() + } +} diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModelTest.kt index 58e1d42ea771..783e2bec9447 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModelTest.kt @@ -6,6 +6,7 @@ import com.duckduckgo.subscriptions.api.Product.ITR import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.subscriptions.api.Subscriptions.EntitlementStatus.Found import com.duckduckgo.subscriptions.api.Subscriptions.EntitlementStatus.NotFound +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.Command.OpenItr import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -14,6 +15,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi @@ -22,11 +24,12 @@ class ItrSettingViewModelTest { val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() private val subscriptions: Subscriptions = mock() + private val pixelSender: SubscriptionPixelSender = mock() private lateinit var viewModel: ItrSettingViewModel @Before fun before() { - viewModel = ItrSettingViewModel(subscriptions, coroutineTestRule.testDispatcherProvider) + viewModel = ItrSettingViewModel(subscriptions, coroutineTestRule.testDispatcherProvider, pixelSender) } @Test @@ -38,6 +41,12 @@ class ItrSettingViewModelTest { } } + @Test + fun whenOnItrThenPixelSent() = runTest { + viewModel.onItr() + verify(pixelSender).reportAppSettingsIdtrClick() + } + @Test fun whenOnResumeIfSubscriptionEmitViewState() = runTest { whenever(subscriptions.getEntitlementStatus(ITR)).thenReturn(Result.success(Found)) diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt index b6ca0dceb448..227760a08866 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt @@ -6,6 +6,7 @@ import com.duckduckgo.subscriptions.api.Product.PIR import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.subscriptions.api.Subscriptions.EntitlementStatus.Found import com.duckduckgo.subscriptions.api.Subscriptions.EntitlementStatus.NotFound +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.Command.OpenPir import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -14,6 +15,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi @@ -22,11 +24,12 @@ class PirSettingViewModelTest { val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() private val subscriptions: Subscriptions = mock() + private val pixelSender: SubscriptionPixelSender = mock() private lateinit var viewModel: PirSettingViewModel @Before fun before() { - viewModel = PirSettingViewModel(subscriptions, coroutineTestRule.testDispatcherProvider) + viewModel = PirSettingViewModel(subscriptions, coroutineTestRule.testDispatcherProvider, pixelSender) } @Test @@ -38,6 +41,12 @@ class PirSettingViewModelTest { } } + @Test + fun whenOnPirThenPixelSent() = runTest { + viewModel.onPir() + verify(pixelSender).reportAppSettingsPirClick() + } + @Test fun whenOnResumeIfEntitlementPresentEmitViewState() = runTest { whenever(subscriptions.getEntitlementStatus(PIR)).thenReturn(Result.success(Found)) diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/AddDeviceViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/AddDeviceViewModelTest.kt index 1358e6a6d6d4..f023a7ba8d2c 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/AddDeviceViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/AddDeviceViewModelTest.kt @@ -4,6 +4,7 @@ import app.cash.turbine.test import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.subscriptions.impl.SubscriptionsData import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.ui.AddDeviceViewModel.Command.AddEmail import com.duckduckgo.subscriptions.impl.ui.AddDeviceViewModel.Command.Error import com.duckduckgo.subscriptions.impl.ui.AddDeviceViewModel.Command.ManageEmail @@ -13,6 +14,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever class AddDeviceViewModelTest { @@ -21,11 +23,12 @@ class AddDeviceViewModelTest { val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() private val subscriptionsManager: SubscriptionsManager = mock() + private val pixelSender: SubscriptionPixelSender = mock() private lateinit var viewModel: AddDeviceViewModel @Before fun before() { - viewModel = AddDeviceViewModel(subscriptionsManager, coroutineTestRule.testDispatcherProvider) + viewModel = AddDeviceViewModel(subscriptionsManager, coroutineTestRule.testDispatcherProvider, pixelSender) } @Test @@ -100,4 +103,10 @@ class AddDeviceViewModelTest { assertNull(awaitItem().email) } } + + @Test + fun whenEnterEmailClickedThenPixelIsSent() = runTest { + viewModel.useEmail() + verify(pixelSender).reportAddDeviceEnterEmailClick() + } } diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt index a239679710dc..8bfba3d86876 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt @@ -5,6 +5,7 @@ import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.subscriptions.impl.RealSubscriptionsManager.Companion.SUBSCRIPTION_NOT_FOUND_ERROR import com.duckduckgo.subscriptions.impl.SubscriptionsData import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.services.Entitlement import com.duckduckgo.subscriptions.impl.ui.RestoreSubscriptionViewModel.Command.Error import com.duckduckgo.subscriptions.impl.ui.RestoreSubscriptionViewModel.Command.RestoreFromEmail @@ -16,6 +17,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever class RestoreSubscriptionViewModelTest { @@ -24,11 +26,16 @@ class RestoreSubscriptionViewModelTest { val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() private val subscriptionsManager: SubscriptionsManager = mock() + private val pixelSender: SubscriptionPixelSender = mock() private lateinit var viewModel: RestoreSubscriptionViewModel @Before fun before() { - viewModel = RestoreSubscriptionViewModel(subscriptionsManager, coroutineTestRule.testDispatcherProvider) + viewModel = RestoreSubscriptionViewModel( + subscriptionsManager = subscriptionsManager, + dispatcherProvider = coroutineTestRule.testDispatcherProvider, + pixelSender = pixelSender, + ) } @Test @@ -95,4 +102,60 @@ class RestoreSubscriptionViewModelTest { assertTrue(result is Success) } } + + @Test + fun whenRestoreFromStoreClickThenPixelIsSent() = runTest { + viewModel.restoreFromStore() + verify(pixelSender).reportActivateSubscriptionRestorePurchaseClick() + } + + @Test + fun whenRestoreFromEmailClickThenPixelIsSent() = runTest { + viewModel.restoreFromEmail() + verify(pixelSender).reportActivateSubscriptionEnterEmailClick() + } + + @Test + fun whenRestoreFromStoreSuccessThenPixelIsSent() = runTest { + whenever(subscriptionsManager.recoverSubscriptionFromStore()).thenReturn( + SubscriptionsData.Success( + email = null, + externalId = "test", + entitlements = listOf(Entitlement(id = "test", product = "test", name = "test")), + ), + ) + + viewModel.restoreFromStore() + verify(pixelSender).reportRestoreUsingStoreSuccess() + } + + @Test + fun whenRestoreFromStoreFailsBecauseThereAreNoEntitlementsThenPixelIsSent() = runTest { + whenever(subscriptionsManager.recoverSubscriptionFromStore()).thenReturn( + SubscriptionsData.Success(email = null, externalId = "test", entitlements = emptyList()), + ) + + viewModel.restoreFromStore() + verify(pixelSender).reportRestoreUsingStoreFailureSubscriptionNotFound() + } + + @Test + fun whenRestoreFromStoreFailsBecauseThereIsNoSubscriptionThenPixelIsSent() = runTest { + whenever(subscriptionsManager.recoverSubscriptionFromStore()).thenReturn( + SubscriptionsData.Failure(SUBSCRIPTION_NOT_FOUND_ERROR), + ) + + viewModel.restoreFromStore() + verify(pixelSender).reportRestoreUsingStoreFailureSubscriptionNotFound() + } + + @Test + fun whenRestoreFromStoreFailsForOtherReasonThenPixelIsSent() = runTest { + whenever(subscriptionsManager.recoverSubscriptionFromStore()).thenReturn( + SubscriptionsData.Failure("bad stuff happened"), + ) + + viewModel.restoreFromStore() + verify(pixelSender).reportRestoreUsingStoreFailureOther() + } } diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt index 0ad056f63f93..4f752f08a246 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt @@ -6,6 +6,7 @@ import com.duckduckgo.subscriptions.impl.Subscription import com.duckduckgo.subscriptions.impl.SubscriptionStatus.AutoRenewable import com.duckduckgo.subscriptions.impl.SubscriptionsConstants import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.FinishSignOut import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.GoToPortal import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.SubscriptionDuration.Monthly @@ -16,6 +17,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever class SubscriptionSettingsViewModelTest { @@ -24,11 +26,12 @@ class SubscriptionSettingsViewModelTest { val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() private val subscriptionsManager: SubscriptionsManager = mock() + private val pixelSender: SubscriptionPixelSender = mock() private lateinit var viewModel: SubscriptionSettingsViewModel @Before fun before() { - viewModel = SubscriptionSettingsViewModel(subscriptionsManager, coroutineTestRule.testDispatcherProvider) + viewModel = SubscriptionSettingsViewModel(subscriptionsManager, coroutineTestRule.testDispatcherProvider, pixelSender) } @Test @@ -115,4 +118,10 @@ class SubscriptionSettingsViewModelTest { cancelAndConsumeRemainingEvents() } } + + @Test + fun whenRemoveFromDeviceThenPixelIsSent() = runTest { + viewModel.removeFromDevice() + verify(pixelSender).reportSubscriptionSettingsRemoveFromDeviceClick() + } } diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt index 2b1ee48893f5..87998cfd2da3 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt @@ -14,6 +14,7 @@ import com.duckduckgo.subscriptions.impl.SubscriptionsConstants import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.YEARLY_PLAN import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender import com.duckduckgo.subscriptions.impl.repository.SubscriptionsRepository import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Companion @@ -31,6 +32,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @@ -43,6 +45,7 @@ class SubscriptionWebViewViewModelTest { private val subscriptionsManager: SubscriptionsManager = mock() private val subscriptionsRepository: SubscriptionsRepository = mock() private val networkProtectionWaitlist: NetworkProtectionWaitlist = mock() + private val pixelSender: SubscriptionPixelSender = mock() private lateinit var viewModel: SubscriptionWebViewViewModel @@ -54,6 +57,7 @@ class SubscriptionWebViewViewModelTest { subscriptionsManager, subscriptionsRepository, networkProtectionWaitlist, + pixelSender, ) } @@ -243,6 +247,77 @@ class SubscriptionWebViewViewModelTest { } } + @Test + fun whenSubscriptionSelectedThenPixelIsSent() = runTest { + viewModel.processJsCallbackMessage( + featureName = "test", + method = "subscriptionSelected", + id = "id", + data = JSONObject("""{"id":"myId"}"""), + ) + verify(pixelSender).reportOfferSubscribeClick() + } + + @Test + fun whenRestorePurchaseClickedThenPixelIsSent() = runTest { + whenever(subscriptionsManager.hasSubscription()).thenReturn(false) + viewModel.processJsCallbackMessage( + featureName = "test", + method = "activateSubscription", + id = null, + data = null, + ) + verify(pixelSender).reportOfferRestorePurchaseClick() + } + + @Test + fun whenActivateOnAnotherDeviceClickedThenPixelIsSent() = runTest { + whenever(subscriptionsManager.hasSubscription()).thenReturn(true) + viewModel.processJsCallbackMessage( + featureName = "test", + method = "activateSubscription", + id = null, + data = null, + ) + verify(pixelSender).reportOnboardingAddDeviceClick() + } + + @Test + fun whenFeatureSelectedAndFeatureIsNetPThenPixelSent() = runTest { + whenever(subscriptionsManager.hasSubscription()).thenReturn(false) + viewModel.processJsCallbackMessage( + featureName = "test", + method = "featureSelected", + id = null, + data = JSONObject("""{"feature":"${SubscriptionsConstants.NETP}"}"""), + ) + verify(pixelSender).reportOnboardingVpnClick() + } + + @Test + fun whenFeatureSelectedAndFeatureIsItrThenPixelIsSent() = runTest { + whenever(subscriptionsManager.hasSubscription()).thenReturn(false) + viewModel.processJsCallbackMessage( + featureName = "test", + method = "featureSelected", + id = null, + data = JSONObject("""{"feature":"${SubscriptionsConstants.ITR}"}"""), + ) + verify(pixelSender).reportOnboardingIdtrClick() + } + + @Test + fun whenFeatureSelectedAndFeatureIsPirThenPixelIsSent() = runTest { + whenever(subscriptionsManager.hasSubscription()).thenReturn(false) + viewModel.processJsCallbackMessage( + featureName = "test", + method = "featureSelected", + id = null, + data = JSONObject("""{"feature":"${SubscriptionsConstants.PIR}"}"""), + ) + verify(pixelSender).reportOnboardingPirClick() + } + private fun getSubscriptionOfferDetails(planId: String): SubscriptionOfferDetails { val subscriptionOfferDetails: SubscriptionOfferDetails = mock() whenever(subscriptionOfferDetails.basePlanId).thenReturn(planId) From 118d282e9ff58070c6390fd7f832a7c2c98aeb33 Mon Sep 17 00:00:00 2001 From: Dax the Deployer Date: Thu, 22 Feb 2024 07:12:30 -0500 Subject: [PATCH 19/19] Updated release notes and version number for new release - 5.190.0 --- app/version/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version/version.properties b/app/version/version.properties index 687523fa6a18..681a2eb1f4a0 100644 --- a/app/version/version.properties +++ b/app/version/version.properties @@ -1 +1 @@ -VERSION=5.189.0 \ No newline at end of file +VERSION=5.190.0 \ No newline at end of file