Skip to content

Commit

Permalink
Adds NetP setting to new section in settings (#3950)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1206070233895163/f 

### Description
See task
### Steps to test this PR
See task
  • Loading branch information
marcosholgado authored Dec 12, 2023
1 parent b778b81 commit 5ade299
Show file tree
Hide file tree
Showing 12 changed files with 606 additions and 6 deletions.
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ subprojects {

for (dependency in dependencies) {
// API modules cannot depend on dagger/anvil
if (projectPath.endsWith("api") && projectPath != ":feature-toggles-api") {
if (projectPath.endsWith("api")
&& projectPath != ":feature-toggles-api"
&& projectPath != ":settings-api")
{
def notAllowedDeps = ["anvil", "dagger"]
if (notAllowedDeps.contains(dependency.name)) {
throw new GradleException("Invalid dependency $projectPath -> " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,5 @@ enum class NetworkProtectionPixelNames(
NETP_TUNNEL_FAILURE_RECOVERED("m_netp_ev_tunnel_failure_recovered_c", enqueue = true),
VPN_SNOOZE_CANCELED("m_vpn_ev_snooze_canceled_c", enqueue = true),
VPN_SNOOZE_CANCELED_DAILY("m_vpn_ev_snooze_canceled_d", enqueue = true),
NETP_SETTINGS_PRESSED("m_netp_ev_setting_pressed_c"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ android {
lintOptions {
baseline file("lint-baseline.xml")
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
anvil {
generateDaggerFactories = true // default is false
}
}

dependencies {
testImplementation 'junit:junit:4.13.1'
anvil project(':anvil-compiler')
implementation project(':app-build-config-api')
implementation project(':anvil-annotations')
Expand All @@ -28,6 +34,8 @@ dependencies {
implementation project(':network-protection-api')
implementation project(':network-protection-impl')
implementation project(':subscriptions-api')
implementation project(':settings-api')
implementation project(':statistics')

implementation AndroidX.appCompat
implementation AndroidX.lifecycle.runtime.ktx
Expand All @@ -38,4 +46,21 @@ dependencies {
implementation Square.retrofit2.retrofit
implementation Square.retrofit2.converter.moshi
implementation "com.squareup.logcat:logcat:_"

// Testing dependencies
testImplementation project(path: ':common-test')
testImplementation "org.mockito.kotlin:mockito-kotlin:_"
testImplementation Testing.junit4
testImplementation AndroidX.archCore.testing
testImplementation AndroidX.core
testImplementation AndroidX.test.ext.junit
testImplementation "androidx.test:runner:_"
testImplementation Testing.robolectric
testImplementation 'app.cash.turbine:turbine:_'
testImplementation project(path: ':common-test')
testImplementation (KotlinX.coroutines.test) {
// https://github.com/Kotlin/kotlinx.coroutines/issues/2023
// conflicts with mockito due to direct inclusion of byte buddy
exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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.networkprotection.subscription.settings

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.common.ui.view.gone
import com.duckduckgo.common.ui.view.show
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.di.scopes.ViewScope
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.duckduckgo.networkprotection.subscription.R
import com.duckduckgo.networkprotection.subscription.databinding.ViewSettingsNetpBinding
import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.Command
import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.Command.OpenNetPScreen
import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.Factory
import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState
import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden
import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending
import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState
import dagger.android.support.AndroidSupportInjection
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

@InjectWith(ViewScope::class)
class ProSettingNetPView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : FrameLayout(context, attrs, defStyle) {

@Inject
lateinit var viewModelFactory: Factory

@Inject
lateinit var globalActivityStarter: GlobalActivityStarter

private var coroutineScope: CoroutineScope? = null

private val binding: ViewSettingsNetpBinding by viewBinding()

private val viewModel: ProSettingNetPViewModel by lazy {
ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[ProSettingNetPViewModel::class.java]
}

override fun onAttachedToWindow() {
AndroidSupportInjection.inject(this)
super.onAttachedToWindow()

ViewTreeLifecycleOwner.get(this)?.lifecycle?.addObserver(viewModel)

binding.netpPSetting.setClickListener {
viewModel.onNetPSettingClicked()
}

@SuppressLint("NoHardcodedCoroutineDispatcher")
coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

viewModel.viewState
.onEach { updateNetPSettings(it.networkProtectionEntryState) }
.launchIn(coroutineScope!!)

viewModel.commands()
.onEach { processCommands(it) }
.launchIn(coroutineScope!!)
}

private fun updateNetPSettings(networkProtectionEntryState: NetPEntryState) {
with(binding.netpPSetting) {
when (networkProtectionEntryState) {
Hidden -> this.gone()
Pending -> {
this.show()
this.setSecondaryText(context.getString(R.string.netpSubscriptionSettingsNeverEnabled))
this.setItemStatus(com.duckduckgo.common.ui.view.listitem.CheckListItem.CheckItemStatus.DISABLED)
}
is ShowState -> {
this.show()
this.setSecondaryText(context.getString(networkProtectionEntryState.subtitle))
this.setItemStatus(networkProtectionEntryState.icon)
}
}
}
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
ViewTreeLifecycleOwner.get(this)?.lifecycle?.removeObserver(viewModel)
coroutineScope?.cancel()
coroutineScope = null
}

private fun processCommands(command: Command) {
when (command) {
is OpenNetPScreen -> {
globalActivityStarter.start(context, command.params)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* 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.networkprotection.subscription.settings

import android.annotation.SuppressLint
import androidx.annotation.StringRes
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.ui.view.listitem.CheckListItem
import com.duckduckgo.common.ui.view.listitem.CheckListItem.CheckItemStatus
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
import com.duckduckgo.networkprotection.api.NetworkProtectionState
import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState
import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTED
import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.CONNECTING
import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.DISCONNECTED
import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist
import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_SETTINGS_PRESSED
import com.duckduckgo.networkprotection.subscription.R
import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden
import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending
import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle
class ProSettingNetPViewModel(
private val networkProtectionWaitlist: NetworkProtectionWaitlist,
private val networkProtectionState: NetworkProtectionState,
private val dispatcherProvider: DispatcherProvider,
private val pixel: Pixel,
) : ViewModel(), DefaultLifecycleObserver {

data class ViewState(val networkProtectionEntryState: NetPEntryState = Hidden)

sealed class Command {
data class OpenNetPScreen(val params: ActivityParams) : Command()
}

sealed class NetPEntryState {
data object Hidden : NetPEntryState()
data object Pending : NetPEntryState()
data class ShowState(
val icon: CheckItemStatus,
@StringRes val subtitle: Int,
) : NetPEntryState()
}

private val command = Channel<Command>(1, BufferOverflow.DROP_OLDEST)
internal fun commands(): Flow<Command> = command.receiveAsFlow()
private val _viewState = MutableStateFlow(ViewState())
val viewState = _viewState.asStateFlow()

override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
viewModelScope.launch {
_viewState.emit(
viewState.value.copy(
networkProtectionEntryState = (if (networkProtectionState.isRunning()) CONNECTED else DISCONNECTED).run {
getNetworkProtectionEntryState(this)
},
),
)
}

networkProtectionState.getConnectionStateFlow()
.onEach {
_viewState.emit(
viewState.value.copy(
networkProtectionEntryState = getNetworkProtectionEntryState(it),
),
)
}.flowOn(dispatcherProvider.main())
.launchIn(viewModelScope)
}

fun onNetPSettingClicked() {
viewModelScope.launch {
val screen = networkProtectionWaitlist.getScreenForCurrentState()
command.send(Command.OpenNetPScreen(screen))
pixel.fire(NETP_SETTINGS_PRESSED)
}
}

private suspend fun getNetworkProtectionEntryState(networkProtectionConnectionState: ConnectionState): NetPEntryState {
return when (val networkProtectionWaitlistState = networkProtectionWaitlist.getState()) {
is NetPWaitlistState.InBeta -> {
if (networkProtectionWaitlistState.termsAccepted || networkProtectionState.isOnboarded()) {
val subtitle = when (networkProtectionConnectionState) {
CONNECTED -> R.string.netpSubscriptionSettingsConnected
CONNECTING -> R.string.netpSubscriptionSettingsConnecting
else -> R.string.netpSubscriptionSettingsDisconnected
}

val netPItemStatus = if (networkProtectionConnectionState != DISCONNECTED) {
CheckListItem.CheckItemStatus.ENABLED
} else {
CheckListItem.CheckItemStatus.WARNING
}

ShowState(
icon = netPItemStatus,
subtitle = subtitle,
)
} else {
Pending
}
}
NetPWaitlistState.NotUnlocked -> Hidden
NetPWaitlistState.PendingInviteCode, NetPWaitlistState.JoinedWaitlist, NetPWaitlistState.VerifySubscription -> Pending
}
}

@Suppress("UNCHECKED_CAST")
class Factory @Inject constructor(
private val networkProtectionWaitlist: NetworkProtectionWaitlist,
private val networkProtectionState: NetworkProtectionState,
private val dispatcherProvider: DispatcherProvider,
private val pixel: Pixel,
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return with(modelClass) {
when {
isAssignableFrom(ProSettingNetPViewModel::class.java) -> ProSettingNetPViewModel(
networkProtectionWaitlist,
networkProtectionState,
dispatcherProvider,
pixel,
)
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.networkprotection.subscription.settings

import android.content.Context
import android.view.View
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.settings.api.PositionKey
import com.duckduckgo.settings.api.ProSettingsPlugin
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject

@ContributesMultibinding(ActivityScope::class)
@PositionKey(150)
class ProSettingsNetP @Inject constructor() : ProSettingsPlugin {
override fun getView(context: Context): View {
return ProSettingNetPView(context)
}
}
Loading

0 comments on commit 5ade299

Please sign in to comment.